From c6b782df70fd683427a390cea9246af3470577df Mon Sep 17 00:00:00 2001 From: Packit Service Date: Feb 03 2021 22:18:47 +0000 Subject: libdazzle-3.28.5 base --- diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..74ff4cc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,40 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf + +[*.[ch]] +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 100 +tab_width = 2 + +[*.css] +indent_size = 2 +tab_size = 2 +indent_style = space + +[*.ui] +indent_size = 2 +tab_size = 2 +indent_style = space + +[*.rst] +indent_size = 3 +tab_size = 3 +indent_style = space + +[*.{xml.in,xml}] +indent_size = 2 +tab_size = 2 +indent_style = space + +[*.json] +indent_size = 4 +tab_size = 4 + +[meson.build] +indent_size = 2 +indent_style = space diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..22cc343 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,6 @@ +Christian Hergert +Dimitris Zenios +Emmanuele Bassi +Garrett Regier +Georges Basile Stavracas Neto +Sébastien Lafargue diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1fcc2e7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,78 @@ +# Contributing + +## Licensing + +Your work is considered a derivative work of the libdazzle codebase and therefore must be licensed as GPLv3+. +You may optionally grant other licensing in addition to GPLv3+ such as LGPLv2.1+ or MIT X11. +However, as part of libdazzle, which is GPLv3+, the combined work will be GPLv3+. + +You do not need to assign us copyright attribution. +It is our belief that you should always retain copyright on your own work. + +## Testing + +When working on a new widget or tool, try to write unit tests to prove the implementation. +Not everything we have in the code base has tests, and ideally that will improve, not get worse. + +## Troubleshooting + +If you configure the meson project with `-Denable_tracing=true` then libdazzle with be built with tracing. +This allows various parts of the code to use `DZL_ENTRY`, `DZL_EXIT` and other tracing macros to log function calls. +You might find this useful in tracking down difficult re-entrancy or simply learn "how does this work". + +If you need to add additional tracing macros to debug a problem, it is probably a good idea to submit a patch to add them. +Chances are someone else will need to debug stuff in the future. + +## Code Style + +We follow the GObject and Gtk coding style. +That is often unfamiliar to those who have not been hacking on GNU projects for the past couple of decades. +However, it is largely entrenched in our community, so we try to be consistent. + +```c +static gboolean +this_is_a_function (GtkWidget *param, + const gchar *another_param, + guint third_param, + GError **error) +{ + g_return_val_if_fail (GTK_IS_WIDGET (param), FALSE); + g_return_val_if_fail (third_param > 10, FALSE); + + if (another_param != NULL) + { + if (!do_some_more_work ()) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Something failed"); + return FALSE; + } + } + +goto_labels_here: + + return TRUE; +} +``` + +```c +void do_something_one (GtkWidget *widget); +void do_something_two (GtkWidget *widget, + GError **error); +gchar *do_something_three (GtkWidget *widget); +gboolean do_something_four (GtkWidget *widget); +``` + + * Notice that we use 2-space indention. + * We indent new blocks {} with 2 spaces, and braces on their own line. We understand that this is confusing at first, but it is rather nice once it becomes familiar. + * No tabs, spaces only. + * Always compare against `NULL` rather than implicit comparisons. This eases ports to other languages and readability. + * Use #define for constants. Try to avoid "magic constants". + * goto labels are fully unindented. + * Align function parameters. + * Align blocks of function declarations in the header. This vastly improves readability and scanning to find what you want. + +If in doubt, look for examples elsewhere in the codebase. + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..10926e8 --- /dev/null +++ b/COPYING @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. + diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..a140227 --- /dev/null +++ b/NEWS @@ -0,0 +1,352 @@ +============== +Version 3.28.5 +============== + +An issue was introduced in menu merging in 3.28.4 and this release fixes +that and many other issues discovered with ASAN. + +Changes in this release: + + • Fuzzy indexes now properly release metadata variant + • Various fixes to g_variant_parse() usage and floating references + • Menu merging has been fixed in DzlMenuButton + • A leak in DzlRing was fixed + • Shortcut closure chains now properly concat GSList links + • A leak in GDateTime usage was fixed + • DzlTreeNode no longer leaks emblem names or GIcon instances + • DockManager now releases it's final transient grab + • DzlStateMachine now properly cleans up property stack items + • Multi-paned and dock-bin now properly unregister GdkWindows + +We recommend all distributions upgrade to this release. + +============== +Version 3.28.4 +============== + +Changes in this release: + + • Fixes for ref counting on titlebar widget + • Fix a leak in DzlBoldingLabel of Pango attribute lists + • Action muxer is now more defensive about string usage + • Menu popover is now more defensive when destroying widgets + • A potential, albeit unlikely, leak was plugged in signal dispatching + via the shortcut engine. + • The shortcut controller is now more defensive during widget destruction. + +============== +Version 3.28.3 +============== + +Changes in this release: + + • Animation uses g_signal_connect_object() to simplify lifecycle tracking + • CPU graph model now re-uses FDs and parse buffers + • Realtime graphs now try harder to detect when the widget is obscured to + reduce overhead in that situation. + • GCancellable chaining tries harder to release state struct. + +============== +Version 3.28.2 +============== + +Changes in this release: + + • theming: avoid creating unnecessary css providers + +============== +Version 3.28.1 +============== + +Changes in this release: + + • Speciy pkg-config package in gir file + • Add Dazzle.StackList.push() to vala wrapper + • DzlFileChooserEntry will now notify when files change + • Shortcuts now ignore caps-lock when activating shortcuts, to be + more like gtk+ itself does. + • transfer ownership fix for registering signals in code + +============== +Version 3.28.0 +============== + +Changes in this release: + + • DzlRecursiveFileMonitor will now translate symlink root directories + up-front to increase the chance that the GFile you see will match + GFile in other GIO based API. This fixes some issues in Builder on + systems such as Fedora Atomic Workstation where home is a symlink. + • dzl_g_date_time_format_for_display() now uses the new %OB format + present in glib 3.56. + +=============== +Version 3.27.92 +=============== + +Changes in this release: + + • Improvements to animation frame timings to more closely match + upstream changes in Gtk. + • DZL_DEFINE_ACTION_GROUP macro will now set initial action state. + • ElasticBin will avoid animating when duration is 0. + +=============== +Version 3.27.91 +=============== + +Changes in this release: + + • libdazzle now properly sets the default ABI visibility for symbols + so that symbols without DZL_AVAILABLE_IN_* macros are not exported. + • DzlTab now supports tooltips using the tab's title. + • Various gtk-doc generation issues have been fixed. + • DzlGraphModel now provides an accessor to get the column count. + • Some fixes for pedantic compiler warnings. + +=============== +Version 3.27.90 +=============== + +Changes in this release: + + • Further protections to ensure we don't follow symlinks in the + directory reaper. + • DzlSignalGroup now uses GWeakRef. Additionally, it allows objects + to invalidate closures instead of doing weak-ref tracking. This + should improve a number of situations. + +============== +Version 3.27.5 +============== + +Changes in this release: + + • A memory leak in the fuzzy index builder was fixed. + • DzlTab now properly sinks a variant floating ref. + • Various memory leak fixes in DzlTree and associated objects. + • The shortcut manager now properly activates mnemonics. + • Signal group ties to be more flexible with re-entrancy situations. + • DirectoryReaper and FileTransfer attempt to be more defensive against + following through symlinks. + +============== +Version 3.27.4 +============== + +Changes in this release: + + • DzlTree has gained a number of improvements for dynamically styling tree + nodes. + • DzlMultiPaned gained a helper to locate a widget at an X,Y coordinate. + • Various memory leak fixes in shortcuts and unit tests. + • DzlListBox allows setting the number of rows to recycle. This may improve + performance in situations where the consuming API knows the maximum number + of rows, saving on widget creation and style propagation costs. + • A new cancellation chain helper that can be used until g_cancellable_chain() + lands upstream. When that happens, this will wrap that API, or be removed + in case it lands in 2.56. + • DzlTree gained support for inserting a child at a specific position. + • Fixes for API deprecations in Gdk. + • Fixes for main thread assertions on Linux. + +============== +Version 3.27.3 +============== + +Changes in this release: + + • A new DzlRecursiveFileMonitor for recursive file monitors. This is + only guaranteed to use a single FD when inotify is used (ie: Linux). + • Fixes for age comparison in directory reaper. + • Improvements to legacy activation in shortcuts. + • Avoid potential spinning in progress-menu-button. + • DzlFileTransfer is a new high-level API to help in copying or moving + directory trees. It is not as smart as libglnx, but it is simpler to + use and we'd like to see this improve. + • DzlTreeBuilder has broken ABI this cycle and gained padding to help + us avoid breaking ABI in the future. Those who only use it from signals + and dzl_tree_builder_new() are not effected by this alone. + • DzlTreeBuilder has also added build-children signal so that it can + disambiguate between building nodes (adding anscillary data) and + creating children in a lazy tree. This does, unfortunately, break + existing code as it needs to switch to using build-children. + +============== +Version 3.27.2 +============== + +Changes in this release: + + • A performance improvement to CSS and resource loading by ensuring + resource directory paths are suffixed with a /. + • Use export macros instead of linker script to simplify ABI conformance + on alternative platforms. + • add missing dzl_preferences_view_new() symbol. + • Various gobject introspection annotation fixes. + • Use g_build_path() when appropriate. + • Add dzl_state_machine_is_state() for checking the current state. + +============== +Version 3.27.1 +============== + + • actions: add DZL_DEFINE_ACTION_GROUP() for creating action groups + +============== +Version 3.26.1 +============== + +Changes in this release: + + • icons: avoid thrashing the GtkIconTheme on startup + • prefs: track destruction state of pref widgets + • macros: add a few internally used macros to public API + • trie: various alignment fixes for Win32 and Win64 + • tests: crib some test data for portable tests + • file-chooser-entry: be lazy with dialog construction + +============== +Version 3.26.0 +============== + +This is our first stable release! Thanks to everyone involved in making +that happen! + +Changes in this release: + + • Fix tab/shift+tab to change focus when using shortcut manager. + • Manually set marshaller and va marshallers on DzlTreeBuilder + which helps slightly with performance of signal emission. + • Various Mingw build fixes. + +=============== +Version 3.25.92 +=============== + +Changes in this release: + + • Enforce GNU11 C standard when configuring project. + • DzlApplicationWindow automatically queries the shortcut manager. + • The shortcut manager now checks GtkApplication registered accelerators + providing an improved migration strategy for applications. + • Lifecycle tests were removed from the test-fuzzy-index as they were + racy via delayed disposal of threaded workers. This should fix unit + tests for various distributions on less common hardware. + • Menu buttons now remove the proper section item when handling GMenuModel + changes. + • Our meson-based build system now checks if version-script is supported + and only applies the ABI map in that case. We may consider switching to + export macros in the future. + • Counters are now disabled on Windows. + • Various compiler pre-processor checks have been improved. + • A fallback for strcasecmp() is included for Windows. + • DzlSuggestion will now use intern'd strings for icon-names to help + reduce the number of duplicate strings in memory. + • Various CSS styling refinements. + • DzlListBox fixed a lingering pointer to the cached row during disposal. + This fixes a crash that would occur if GObjectClass.dispose() was called + multiple times. + +=============== +Version 3.25.91 +=============== + +Changes in this release: + + • DzlDirectoryReaper now properly removes recursive directories + • DzlListStoreAdapter is a new GListModel->GtkTreeModel adapter + • DzlSignalGroup had an overzealous assertion which was removed + and corner case handled. + • DzlShortcutTheme properly handles closure chain parse failure case + • DzlShortcutManager can handle DzlShortcutEntry that are not static + allocations. + +=============== +Version 3.25.90 +=============== + +Changes in this release: + + • DzlSignalGroup and DzlBIndingGroup were made LGPLv2.1+ so they can be + copied into projects that cannot link against libdazzle. + • DzlDockBin got a number of bug fixes. + • Various compilation fixes for less used compilers. + • DzlTree gained support for "expanded icon names". + • DzlTree has a new "always-expand" property to ensure it stays expanded. + • DzlDockStack no longer requires homogeneous sized children. + • DzlStackList transition animations have been tweaked to more closely + reflect physical locality. + • Calculated animation duration was sped up 1.5x. + +============== +Version 3.25.5 +============== + +We are quickly releasing a followup to 3.25.4 to fix a number of bugs with +the shortcut engine that were found as Builder transitioned to it. + + • DzlShortcutManager is not in control of event delivery. This allows the + capture/bubble phase to act as expected and still integrate with + GtkBindingSet. + • DzlSuggestionEntry now properly places the popover window when using the + X11 backend. + • Various license blurbs have been updated to reflect reality. + +Thanks to everyone who was quick to report bugs. + +============== +Version 3.25.4 +============== + +We are mid-development cycle for GNOME 3.26 and so as you can imagine, we are +busy adding the features we need to libdazzle. Given how early the project is, +the pace of development has been quite rapid. + + • Updated build system to make things easier to maintain. + • Soname and libtool-like versioning has been added. Like GObject and Gtk, + we don't intend on bumping this when we break ABI during development + snapshots. Only in the stable series. + • Consistent LGPL-2.1+ usage. + • DzlJoinedMenu is a new GMenuModel implementation that allows you to + concatenate multiple GMenuModel implementations. This can be useful + in various plugin scenarios. + • DzlMultiPaned has gone through more improvements. We know of a few more + that need to be shaken out before 3.26, but things are stabilizing. + • Utilities to proxy action groups between GtkWidgets. + • WIN64 build fixes for DzlTrie. + • Examples have been added for DzlApplication and the CPU graph. + • Many theming improvements. + • DzlDockBin now tries to unpin a panel when animating out. This can give + the effect of smoother transitions where the cost of resizing the main + window content is high (such as with textviews). + • Utilities for insertion with GtkListStore have been added. + • DzlPropertiesGroup is a new GActionGroup that can help simplify exposing + groups of properties on a single object as a GActionGroup. + • The DzlMenuMerger can now perform multi-phase sorting to increase the + chances of ideal placement with before/after among multiple menu items. + • A new DzlMenuButton has been added that has a consistent style for a few + forms of MenuButton type scenarios. It can have icons, accelerators and + we expect in the future to keep the accelerator in sync with the active + shortcut theme. Some extended attributes with menus.ui are used. + • DzlSuggestionEntry now supports a position function which can specify being + relative to the window or to the entry widget. Various position functions + are provided as a convenience. + • The shortcut engine has gained support for CAPTURE/BUBBLE semantics. + • The shortcut engine has gained support for specifying global shortcuts + which can be activated outside the chain of focus. + +We hope you enjoy using libdazzle! + + +============== +Version 3.25.3 +============== + +This is our first release of libdazzle. While we would love for a perfect first +release, we expect you'll find issues. So please help us get things stable as +we move towards GNOME 3.26. + +Our focus is building a utility kit GNOME applications. If you want to see +practical examples of using Dazzle, we suggestion looking at the GNOME Builder +project which uses Dazzle extensively. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d87776 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Dazzle + +The libdazzle library is a companion library to GObject and Gtk+. +It provides various features that we wish were in the underlying library but cannot for various reasons. +In most cases, they are wildly out of scope for those libraries. +In other cases, our design isn't quite generic enough to work for everyone. + +While we don't want to blind our users, we do think of dazzle as something you haven't seen before. +As we improve our implementations in libdazzle, we do think that bits of libdazzle can be migrated into upstream projects. + +Currently, the primary consumer of libdazzle is the Builder IDE. +Most of this code was extracted from Builder so that it could be used by others. + +The libdazzle project is heavily opinionated, and tends to gravitate towards design that matches the GNOME 3 human interface guidelines. + +## Language Support + +libdazzle, as you might imagine, is written in C. +We find C the most convenient language when it comes down to interoperability with other language runtimes. +libdazzle supports GObject Introspection and vapi meaning you can use libdazzle from a wide variety of languages including: + + - Python 2.7 or 3.x + - JavaScript (using Gjs/Spidermonkey) + - Lua/Luajit + - Perl + - Rust + - Vala + +and many others that implement binding support for GObject Introspection. + +## License + +libdazzle is licensed under the GPLv3+. +We *DO NOT* require copyright attribution to contribute to libdazzle. + +## Building + +We use the meson (and thereby Ninja) build system for libdazzle. +The quickest way to get going is to do the following: + +```sh +meson . build +cd build +ninja +ninja install +``` + +If you need control over installation paths, see `meson --help`. +Here is a fairly common way to configure libdazzle. + +```sh +meson --prefix=/opt/gnome --libdir=/opt/gnome/lib . build +``` + +## Components + +libdazzle has a wide range of components from utilities for `GIO`, widgets for `Gtk`, an animation framework, state machines, paneling and high-performance counters. + +### TODO: overview of components + +Until we have an overrview here, check out the src/ directory. + diff --git a/data/themes/Adwaita-dark.css b/data/themes/Adwaita-dark.css new file mode 100644 index 0000000..be12030 --- /dev/null +++ b/data/themes/Adwaita-dark.css @@ -0,0 +1,8 @@ +/* Theme agnostic or base-layer CSS styling */ +@import url("resource:///org/gnome/dazzle/themes/shared.css"); + +/* Theme specific styling */ +@import url("resource:///org/gnome/dazzle/themes/Adwaita/Adwaita-panels.css"); + +@import url("resource:///org/gnome/dazzle/themes/Adwaita/Adwaita-dark-graphs.css"); +@import url("resource:///org/gnome/dazzle/themes/Adwaita/Adwaita-dark-searchbar.css"); diff --git a/data/themes/Adwaita.css b/data/themes/Adwaita.css new file mode 100644 index 0000000..5acd537 --- /dev/null +++ b/data/themes/Adwaita.css @@ -0,0 +1,7 @@ +/* Theme agnostic or base-layer CSS styling */ +@import url("resource:///org/gnome/dazzle/themes/shared.css"); + +/* Theme specific styling */ +@import url("resource:///org/gnome/dazzle/themes/Adwaita/Adwaita-graphs.css"); +@import url("resource:///org/gnome/dazzle/themes/Adwaita/Adwaita-panels.css"); +@import url("resource:///org/gnome/dazzle/themes/Adwaita/Adwaita-searchbar.css"); diff --git a/data/themes/Adwaita/Adwaita-dark-graphs.css b/data/themes/Adwaita/Adwaita-dark-graphs.css new file mode 100644 index 0000000..0026149 --- /dev/null +++ b/data/themes/Adwaita/Adwaita-dark-graphs.css @@ -0,0 +1,6 @@ +dzlgraphview { + background-color: #232729; + background-size: 8px 8px; + background-image: repeating-linear-gradient(0deg, #2e2e2e, #2e2e2e 1px, transparent 1px, transparent 8px), + repeating-linear-gradient(-90deg, #2e2e2e, #2e2e2e 1px, transparent 1px, transparent 8px); +} diff --git a/data/themes/Adwaita/Adwaita-dark-searchbar.css b/data/themes/Adwaita/Adwaita-dark-searchbar.css new file mode 100644 index 0000000..c6d6e1f --- /dev/null +++ b/data/themes/Adwaita/Adwaita-dark-searchbar.css @@ -0,0 +1,8 @@ +dzlsearchbar box.search-bar { + background-color: #2c3133; + box-shadow: 0 4px 3px -5px #2c3030 inset, + 0 -1px 0 #1c1f1f inset; +} +dzlsearchbar:backdrop box.search-bar { + box-shadow: 0 -1px 0 #2c3030 inset; +} diff --git a/data/themes/Adwaita/Adwaita-graphs.css b/data/themes/Adwaita/Adwaita-graphs.css new file mode 100644 index 0000000..cd8e8f1 --- /dev/null +++ b/data/themes/Adwaita/Adwaita-graphs.css @@ -0,0 +1,6 @@ +dzlgraphview.view { + background-color: #f6f7f8; + background-size: 8px 8px; + background-image: repeating-linear-gradient(0deg, #f0f1f2, #f0f1f2 1px, transparent 1px, transparent 8px), + repeating-linear-gradient(-90deg, #f0f1f2, #f0f1f2 1px, transparent 1px, transparent 8px); +} diff --git a/data/themes/Adwaita/Adwaita-panels.css b/data/themes/Adwaita/Adwaita-panels.css new file mode 100644 index 0000000..7d6ae4d --- /dev/null +++ b/data/themes/Adwaita/Adwaita-panels.css @@ -0,0 +1,38 @@ +dzldockstack dzltabstrip { + padding-top: 2px; + background-color: @content_view_bg; + color: @theme_fg_color; + border-bottom: 1px solid mix(@theme_bg_color, @borders, 0.25); +} + +dzldockstack dzltabstrip button.control, +dzldockstack dzltabstrip dzltab { + border-bottom: 3px; + border-bottom-style: solid; + border-bottom-color: transparent; + + margin-left: 6px; + margin-right: 6px; + + color: @theme_fg_color; +} + +dzldockstack dzltabstrip dzltab:active { + border-bottom-color: @theme_selected_bg_color; +} + +dzldockstack dzltabstrip button.control:last-child { + margin-right: 16px; +} + +dzldockstack dzltabstrip button.control:hover, +dzldockstack dzltabstrip dzltab:hover { + border-bottom-color: mix(@borders, @theme_bg_color, 0.25); +} + +dzldockstack dzltabstrip button.control:hover:checked, +dzldockstack dzltabstrip button.control:checked, +dzldockstack dzltabstrip dzltab:hover:checked, +dzldockstack dzltabstrip dzltab:checked { + border-bottom-color: @theme_selected_bg_color; +} diff --git a/data/themes/Adwaita/Adwaita-searchbar.css b/data/themes/Adwaita/Adwaita-searchbar.css new file mode 100644 index 0000000..17ade5f --- /dev/null +++ b/data/themes/Adwaita/Adwaita-searchbar.css @@ -0,0 +1,9 @@ +dzlsearchbar box.search-bar { + background-color: #d6d6d6; + box-shadow: 0 4px 3px -5px #aaa inset, + 0 -1px 0 #a1a1a1 inset; +} +dzlsearchbar:backdrop box.search-bar { + background-color: #d5d5d5; + box-shadow: none; +} diff --git a/data/themes/Arc.css b/data/themes/Arc.css new file mode 100644 index 0000000..f2be5ca --- /dev/null +++ b/data/themes/Arc.css @@ -0,0 +1,5 @@ +/* Theme agnostic or base-layer CSS styling */ +@import url("resource:///org/gnome/dazzle/themes/shared.css"); + +/* Theme specific styling */ +@import url("resource:///org/gnome/dazzle/themes/Arc/Arc-panels.css"); diff --git a/data/themes/Arc/Arc-panels.css b/data/themes/Arc/Arc-panels.css new file mode 100644 index 0000000..419ef49 --- /dev/null +++ b/data/themes/Arc/Arc-panels.css @@ -0,0 +1,43 @@ +dzldockstack dzltabstrip { + background-color: @theme_bg_color; +} + +dzldockstack dzltabstrip button.control:hover:checked, +dzldockstack dzltabstrip button.control:checked, +dzldockstack dzltabstrip dzltab:hover:checked, +dzldockstack dzltabstrip dzltab:checked { + color: @theme_fg_color; + background-color: @theme_base_color; + border-color: @borders; +} + +dzldockstack dzltabstrip dzltab { + font-size: 0.9em; + margin-top: 3px; + padding-bottom: 3px; + border-style: solid; + border-color: transparent; + border-right-width: 1px; + border-left-width: 1px; + border-top-width: 1px; + border-bottom-width: 0px; +} + +dzldockstack dzltabstrip dzltab:backdrop:checked, +dzldockstack dzltabstrip dzltab:backdrop { + background-color: alpha(@theme_base_color, 0.25); +} + +dzldockstack dzltabstrip dzltab:checked { + border-left-color: @borders; + border-right-color: @borders; + border-top-color: @borders; +} + +dzldockstack dzltabstrip dzltab:checked:first-child { + border-left-color: alpha(@theme_base_color, 0.25); +} + +dzldockstack dzltabstrip dzltab:checked:last-child { + border-right-color: alpha(@theme_base_color, 0.25); +} diff --git a/data/themes/shared.css b/data/themes/shared.css new file mode 100644 index 0000000..91a2a32 --- /dev/null +++ b/data/themes/shared.css @@ -0,0 +1,10 @@ +/* Theme agnostic or base-layer CSS styling */ +@import url("resource:///org/gnome/dazzle/themes/shared/shared-graphs.css"); +@import url("resource:///org/gnome/dazzle/themes/shared/shared-menus.css"); +@import url("resource:///org/gnome/dazzle/themes/shared/shared-panels.css"); +@import url("resource:///org/gnome/dazzle/themes/shared/shared-pathbar.css"); +@import url("resource:///org/gnome/dazzle/themes/shared/shared-pillbox.css"); +@import url("resource:///org/gnome/dazzle/themes/shared/shared-preferences.css"); +@import url("resource:///org/gnome/dazzle/themes/shared/shared-progressbutton.css"); +@import url("resource:///org/gnome/dazzle/themes/shared/shared-stacklist.css"); +@import url("resource:///org/gnome/dazzle/themes/shared/shared-suggestions.css"); diff --git a/data/themes/shared/shared-graphs.css b/data/themes/shared/shared-graphs.css new file mode 100644 index 0000000..df9b786 --- /dev/null +++ b/data/themes/shared/shared-graphs.css @@ -0,0 +1,6 @@ +dzlgraphview.view { + background-color: transparent; + background-size: 8px 8px; + background-image: repeating-linear-gradient(0deg, alpha(@borders,0.25), alpha(@borders,0.25) 1px, transparent 1px, transparent 8px), + repeating-linear-gradient(-90deg, alpha(@borders,0.25), alpha(@borders,0.25) 1px, transparent 1px, transparent 8px); +} diff --git a/data/themes/shared/shared-menus.css b/data/themes/shared/shared-menus.css new file mode 100644 index 0000000..8a0a78d --- /dev/null +++ b/data/themes/shared/shared-menus.css @@ -0,0 +1,45 @@ +button.dzlmenubutton > box { + margin-left: 3px; + margin-right: 3px; +} +button.dzlmenubutton > box > image.arrow { + margin-left: 3px; +} +popover.dzlmenubutton > box { + margin: 15px 15px 15px 12px; +} +dzlmenubuttonsection:first-child separator { + background: none; + margin-top: 0px; + margin-bottom: 0px; +} +dzlmenubuttonsection:not(:first-child) separator { + border-color: @borders; + margin-top: 8px; + margin-bottom: 8px; + margin-left: 10px; + margin-right: 10px; +} +dzlmenubuttonsection > label.title { + font-weight: bold; + font-size: 0.8333em; + opacity: 0.5; + margin-bottom: 6px; +} +dzlmenubuttonsection box.vertical checkbutton.dzlmenubuttonitem, +dzlmenubuttonsection box.vertical button.dzlmenubuttonitem { + border: none; + background: none; + box-shadow: none; + padding: 1px 10px; + outline-offset: -2px; +} +dzlmenubuttonsection box.vertical checkbutton.dzlmenubuttonitem:hover, +dzlmenubuttonsection box.vertical button.dzlmenubuttonitem:hover { + background-color: alpha(@theme_fg_color, 0.05); +} +dzlmenubuttonsection box.vertical checkbutton.dzlmenubuttonitem > box > image:first-child, +dzlmenubuttonsection box.vertical button.dzlmenubuttonitem > box > image:first-child { + margin: 3px 8px 3px 0px; + color: @theme_fg_color; +} diff --git a/data/themes/shared/shared-panels.css b/data/themes/shared/shared-panels.css new file mode 100644 index 0000000..3581ebe --- /dev/null +++ b/data/themes/shared/shared-panels.css @@ -0,0 +1,45 @@ + +dzldockstack dzltabstrip button.control, +dzldockstack dzltabstrip dzltab button, +dzldockstack dzltabstrip dzltab { + transition-duration: 200ms; +} + +dzldockstack dzltabstrip dzltab button { + padding: 0; + margin: 0; + border: none; + background: none; + box-shadow: none; +} + +dzldockstack dzltabstrip button.control, +dzldockstack dzltabstrip dzltab { + background: transparent; + box-shadow: none; + border: none; + padding: 0; + margin: 0; +} + +dzldockpaned .handle { + border: 1px solid @borders; +} + +.dzldockbinedge { + background-color: @content_view_bg; + border-color: @borders; + border-style: solid; +} + +.dzldockbinedge.right { + border-left-width: 1px; +} + +.dzldockbinedge.left { + border-right-width: 1px; +} + +.dzldockbinedge.bottom { + border-top-width: 1px; +} diff --git a/data/themes/shared/shared-pathbar.css b/data/themes/shared/shared-pathbar.css new file mode 100644 index 0000000..836b75b --- /dev/null +++ b/data/themes/shared/shared-pathbar.css @@ -0,0 +1,34 @@ +dzlpathbar button { + background: none; + border: none; + box-shadow: none; + border-radius: 0px; + border-top: 2px solid transparent; + border-bottom: 2px solid transparent; + opacity: 0.75; + padding: 3px 3px 0px 3px; +} + +dzlpathbar button image { + color: alpha(currentColor, 0.8); +} + +dzlpathbar label.separator { + font-size: 0.83333em; + opacity: 0.55; +} + +dzlpathbar button:checked { + border-bottom: 2px solid @theme_selected_bg_color; + font-weight: bold; + opacity: 1; +} + +dzlpathbar button:checked:backdrop { + border-bottom-color: alpha(@borders,.75); +} + +dzlpathbar button:hover { + opacity: 0.9; + border-bottom-color: @borders; +} diff --git a/data/themes/shared/shared-pillbox.css b/data/themes/shared/shared-pillbox.css new file mode 100644 index 0000000..c4f89a8 --- /dev/null +++ b/data/themes/shared/shared-pillbox.css @@ -0,0 +1,9 @@ +dzlpillbox { + border-radius: 3px; + background-color: mix(@borders, @theme_bg_color, .5); +} + +dzlpillbox:backdrop { + border-radius: 3px; + background-color: mix(@borders, @theme_unfocused_bg_color, .75); +} diff --git a/data/themes/shared/shared-preferences.css b/data/themes/shared/shared-preferences.css new file mode 100644 index 0000000..0b3079c --- /dev/null +++ b/data/themes/shared/shared-preferences.css @@ -0,0 +1,42 @@ +entry.preferences-search { + border: none; + border-right: 1px solid alpha(@borders, 0.55); + border-bottom: 1px solid alpha(@borders, 0.55); + border-radius: 0; +} + +dzlpreferencesview stacksidebar list { + background-color: @content_view_bg; + border-right: 1px solid alpha(@borders, 0.55); +} + +dzlpreferencesview stacksidebar list separator { + background-color: transparent; +} + +dzlpreferencesview list row entry { + background: transparent; + border: none; + padding: 0; + padding-left: 5px; + border-radius: 3px; + margin: -5px; +} + +dzlpreferencesview list row spinbutton entry { + margin-left: 2px; +} + +dzlpreferencesview dzlpreferencesgroup list row { + padding: 10px; + border-bottom: 1px solid alpha(@borders, 0.2); +} + +dzlpreferencesview dzlpreferencesgroup list row:last-child { + border-bottom: none; +} + +dzlpreferencesview dzlpreferencesgroup list entry { + background: none; + min-height: 0px; +} diff --git a/data/themes/shared/shared-progressbutton.css b/data/themes/shared/shared-progressbutton.css new file mode 100644 index 0000000..f734451 --- /dev/null +++ b/data/themes/shared/shared-progressbutton.css @@ -0,0 +1,10 @@ +.install-progress { + background-image: linear-gradient(to top, @theme_selected_bg_color 2px, alpha(@theme_selected_bg_color, 0) 2px); + background-repeat: no-repeat; + background-position: 0 bottom; + transition: none; +} + +.install-progress:dir(rtl) { + background-position: 100% bottom; +} diff --git a/data/themes/shared/shared-stacklist.css b/data/themes/shared/shared-stacklist.css new file mode 100644 index 0000000..8dc49fa --- /dev/null +++ b/data/themes/shared/shared-stacklist.css @@ -0,0 +1,41 @@ +dzlstacklist row { + padding: 3px 0; + color: @theme_fg_color; +} +dzlstacklist row label.dim-label { + font-size: smaller; +} +dzlstacklist row:backdrop { + color: @theme_unfocused_fg_color; +} +dzlstacklist row > box > image:first-child { + margin: 6px 12px; + min-height: 16px; + min-width: 16px; + opacity: 0.7; +} +dzlstacklist list.stack-header { + background-color: @theme_base_color; + border-bottom: 1px solid @borders; +} +dzlstacklist list.stack-header:backdrop { + background-color: @theme_unfocused_base_color; + border-color: @unfocused_borders; +} +dzlstacklist list.stack-header row:not(:last-child) { + border-bottom: 1px solid rgba(0,0,0,0.1); +} +dzlstacklist list.stack-header row:last-child { + border-bottom: 1px solid transparent; +} +dzlstacklist list:not(.stack-header) row:active, +dzlstacklist list:not(.stack-header) row:selected, +dzlstacklist list.stack-header row:not(:last-child):active, +dzlstacklist list.stack-header row:not(:last-child):selected { + color: @theme_selected_fg_color; + background-color: @theme_selected_bg_color; +} +dzlstacklist row.animating label:not(.dim-label), +dzlstacklist list.stack-header row:last-child label:not(.dim-label) { + font-weight: bold; +} diff --git a/data/themes/shared/shared-suggestions.css b/data/themes/shared/shared-suggestions.css new file mode 100644 index 0000000..a7fcc3a --- /dev/null +++ b/data/themes/shared/shared-suggestions.css @@ -0,0 +1,50 @@ +dzlsuggestionpopover { + background: transparent; +} + +dzlsuggestionpopover > revealer > box { + margin: 5px 6px 6px 6px; + border: 1px solid @borders; + box-shadow: 0px 0px 3px @wm_shadow; + padding: 3px 3px 0px 3px; + background: @content_view_bg; + border-radius: 5px; +} + +dzlsuggestionpopover > revealer > box > elastic > scrolledwindow { + background: alpha(@content_view_bg, 0.97); +} + +dzlsuggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list { + background: transparent; +} + +dzlsuggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row:first-child { + transition: none; +} + +dzlsuggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row { + border-radius: 3px; + transition: none; +} + +dzlsuggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row:selected .title { + color: @theme_selected_fg_color; +} + +dzlsuggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row .title { + color: @theme_fg_color; +} + +dzlsuggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row > box { + margin: 4px 8px 4px 8px; +} + +dzlsuggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row:last-child { + border-bottom: none; + margin-bottom: 3px; +} + +dzlsuggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row > box > image:first-child { + min-width: 16px; +} diff --git a/doc/dazzle-docs.sgml b/doc/dazzle-docs.sgml new file mode 100644 index 0000000..a3a946e --- /dev/null +++ b/doc/dazzle-docs.sgml @@ -0,0 +1,344 @@ + + + + %gtkdocentities; +]> + + + &package_name; Reference Manual + + This document is the API reference for for &package_name; &package_version;. + + Dazzle is a library to help you write beautiful and efficient Gtk applications. + It includes a collection of dazzling Gtk widgets, data structures, search engines, + a shortcut engine, panels, desktop integration, and those missing pieces from + common libraries that help you write cleaner and safer code. + + + The latest version of this API reference is also available + online. + + + If you find any issues in this API reference, please report it + using the online + bug reporting tool at bugzilla.gnome.org. + + + + + 2014-2017 + Christian Hergert + + + + + Permission is granted to copy, distribute and/or modify this + document under the terms of the GNU Free + Documentation License, Version 1.1 or any later + version published by the Free Software Foundation with no + Invariant Sections, no Front-Cover Texts, and no Back-Cover + Texts. You may obtain a copy of the GNU Free + Documentation License from the Free Software + Foundation by visiting their Web site or by writing + to: + +
+ The Free Software Foundation, Inc., + 59 Temple Place - Suite 330, + Boston, MA 02111-1307, + USA +
+
+
+
+ + + Animations + + + Dazzle provides utilities for animations of GtkWidgets and GObjects. + They can optionally be synchronized to a frame clock to ensure that you + update settings at an optimal time in the draw cycle. + + + + + + + + + Signals and Bindings + + + Managing groups of signals and properties can be challanging to get + right. It takes lots of code and detail to ensure you track object + lifetimes correctly. + + + To simplify this, Dazzle provides helper objects to attach to bind + multiple properties or connect to multiple signals. You can attach + and detach them as a set. + + + + + + + + Applications + + + To simplify application creation, we provided a base application to + automatically integrate features from libdazzle. Such features include + theme management, keyboard shortcut controllers, icon loading, and menu + merging. + + + To take advantage of these features, simply subclass the applicaion object. + + + + + + + + + + Application Preferences + + + + + + + + + + + + + + + Shortcuts + + + + + + + + + + + + + + + + + + Window Panels + + + Many content creation applications need some sort of panel system. Dazzle + provides flexible panel widgets and docks for this purpose. + + + + + + + + + + + + + + + + + + + + Searching + + In-Memory Fuzzy Indexes + + + + Disk-based Fuzzy Indexes + + + + + + + + + + + + Browser-like auto-completion + + + Auto-completion in browsers are fairly ubiquitous these days. Having a + widget that looks and feels like them can be useful for applications to + feel famliar to users. The suggestion entry of Dazzle can do this. + You'll need to bring your own search algorithm though. + + + + + + + + + + + Lazy Tree Building + + + Building and populating tree views lazily can be a difficult process. + The tree builder and associated classes in Dazzle makes this fast, + efficient, and easy to perform lazy content creation. + + + + + + + + + + Recycling List Boxes + + + One difficulty with GtkListBox is the performance related to creating + and destroying rows. To help lower this overhead, Dazzle provides a + listbox subclass that re-uses existing rows. + + + + + + + + Path Bars + + + + + + + Realtime Graphs + + + + + + + + + + + Performance Counters + + + + + Additional Widgets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Data Structures + + + + + + + Action Integration + + + + + + + + Asynchronous Caching + + + + + Directory Management + + + + + + State Machines + + + + + Utility API + + + + + + + + + + + + + + + Object Hierarchy + + + + + API Index + + + + + Index of deprecated API + + + + +
diff --git a/doc/meson.build b/doc/meson.build new file mode 100644 index 0000000..39759a0 --- /dev/null +++ b/doc/meson.build @@ -0,0 +1,55 @@ +subdir('xml') + +private_headers = ['config.h'] + +private_src_headers = [ + 'dazzle.h', + 'graphing/dzl-column-private.h', + 'graphing/dzl-graph-column-private.h', + 'menus/dzl-menu-button-section.h', + 'panel/dzl-dock-bin-edge-private.h', + 'panel/dzl-dock-paned-private.h', + 'panel/dzl-tab-private.h', + 'prefs/dzl-preferences-bin-private.h', + 'prefs/dzl-preferences-group-private.h', + 'prefs/dzl-preferences-page-private.h', + 'search/dzl-fuzzy-index-private.h', + 'shortcuts/dzl-shortcut-private.h', + 'shortcuts/dzl-shortcuts-shortcut-private.h', + 'shortcuts/dzl-shortcuts-window-private.h', + 'tree/dzl-tree-private.h', + 'util/dzl-util-private.h', + 'widgets/dzl-list-box-private.h', + 'widgets/dzl-rect-helper.h', +] + +foreach private_src_header : private_src_headers + private_headers += [join_paths(meson.source_root(), 'src', private_src_header)] +endforeach + +glib_prefix = dependency('glib-2.0').get_pkgconfig_variable('prefix') +glib_docpath = join_paths(glib_prefix, 'share', 'gtk-doc', 'html') +docpath = join_paths(get_option('datadir'), 'gtk-doc', 'html') + +gnome.gtkdoc('libdazzle', + main_xml: 'dazzle-docs.sgml', + src_dir: [ + join_paths(meson.source_root(), 'src'), + join_paths(meson.build_root(), 'src'), + ], + dependencies: libdazzle_dep, + gobject_typesfile: 'libdazzle.types', + scan_args: [ + '--rebuild-types', + '--ignore-headers=' + ' '.join(private_headers), + ], + fixxref_args: [ + '--html-dir=@0@'.format(docpath), + '--extra-dir=@0@'.format(join_paths(glib_docpath, 'glib')), + '--extra-dir=@0@'.format(join_paths(glib_docpath, 'gobject')), + '--extra-dir=@0@'.format(join_paths(glib_docpath, 'gio')), + '--extra-dir=@0@'.format(join_paths(glib_docpath, 'gi')), + '--extra-dir=@0@'.format(join_paths(glib_docpath, 'gtk3')), + ], + install_dir: 'libdazzle', + install: true) diff --git a/doc/xml/gtkdocentities.ent.in b/doc/xml/gtkdocentities.ent.in new file mode 100644 index 0000000..f12c9ff --- /dev/null +++ b/doc/xml/gtkdocentities.ent.in @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/doc/xml/meson.build b/doc/xml/meson.build new file mode 100644 index 0000000..e0ba788 --- /dev/null +++ b/doc/xml/meson.build @@ -0,0 +1,11 @@ +ent_conf = configuration_data() +ent_conf.set('PACKAGE', 'Dazzle') +ent_conf.set('PACKAGE_BUGREPORT', 'https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-builder') +ent_conf.set('PACKAGE_NAME', 'Dazzle') +ent_conf.set('PACKAGE_STRING', 'libdazzle') +ent_conf.set('PACKAGE_TARNAME', 'libdazzle-' + meson.project_version()) +ent_conf.set('PACKAGE_URL', 'http://wiki.gnome.org/Apps/Builder') +ent_conf.set('PACKAGE_VERSION', meson.project_version()) +ent_conf.set('PACKAGE_API_VERSION', apiversion) +configure_file(input: 'gtkdocentities.ent.in', output: 'gtkdocentities.ent', configuration: ent_conf) + diff --git a/examples/app/README.md b/examples/app/README.md new file mode 100644 index 0000000..0c5a29d --- /dev/null +++ b/examples/app/README.md @@ -0,0 +1,6 @@ +# Example Application + +This directory contains a sample application built using various components of libdazzle. +It can service as a tutorial of how to build your application to get the most out of libdazzle. + + diff --git a/examples/app/example-application.c b/examples/app/example-application.c new file mode 100644 index 0000000..681db96 --- /dev/null +++ b/examples/app/example-application.c @@ -0,0 +1,87 @@ +#include "example-application.h" +#include "example-window.h" + +struct _ExampleApplication +{ + DzlApplication parent_instance; +}; + +G_DEFINE_TYPE (ExampleApplication, example_application, DZL_TYPE_APPLICATION) + +static void +example_application_activate (GApplication *app) +{ + GtkWindow *window; + + window = gtk_application_get_active_window (GTK_APPLICATION (app)); + + if (window == NULL) + window = g_object_new (EXAMPLE_TYPE_WINDOW, + "application", app, + "default-width", 800, + "default-height", 600, + "title", "Example Window", + NULL); + + gtk_window_present (window); +} + +static void +example_application_class_init (ExampleApplicationClass *klass) +{ + GApplicationClass *app_class = G_APPLICATION_CLASS (klass); + + app_class->activate = example_application_activate; +} + +static void +about_activate (GSimpleAction *action, + GVariant *variant, + gpointer user_data) +{ + GtkAboutDialog *dialog; + + dialog = g_object_new (GTK_TYPE_ABOUT_DIALOG, + "copyright", "Copyright 2017 Christian Hergert", + "logo-icon-name", "org.gnome.clocks", + "website", "https://wiki.gnome.org/Apps/Builder", + "version", DZL_VERSION_S, + NULL); + + gtk_window_present (GTK_WINDOW (dialog)); +} + +static void +quit_activate (GSimpleAction *action, + GVariant *variant, + gpointer user_data) +{ + g_application_quit (G_APPLICATION (user_data)); +} + +static void +shortcuts_activate (GSimpleAction *action, + GVariant *variant, + gpointer user_data) +{ + DzlShortcutsWindow *window; + DzlShortcutManager *manager; + + manager = dzl_application_get_shortcut_manager (user_data); + + window = g_object_new (DZL_TYPE_SHORTCUTS_WINDOW, NULL); + dzl_shortcut_manager_add_shortcuts_to_window (manager, window); + gtk_window_present (GTK_WINDOW (window)); +} + +static void +example_application_init (ExampleApplication *self) +{ + static GActionEntry entries[] = { + { "about", about_activate }, + { "quit", quit_activate }, + { "shortcuts", shortcuts_activate }, + }; + + g_action_map_add_action_entries (G_ACTION_MAP (self), entries, G_N_ELEMENTS (entries), self); +} diff --git a/examples/app/example-application.h b/examples/app/example-application.h new file mode 100644 index 0000000..2b8d624 --- /dev/null +++ b/examples/app/example-application.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define EXAMPLE_TYPE_APPLICATION (example_application_get_type()) + +G_DECLARE_FINAL_TYPE (ExampleApplication, example_application, EXAMPLE, APPLICATION, DzlApplication) + +G_END_DECLS diff --git a/examples/app/example-document-view.c b/examples/app/example-document-view.c new file mode 100644 index 0000000..dd9e1cf --- /dev/null +++ b/examples/app/example-document-view.c @@ -0,0 +1,126 @@ +#include "example-document-view.h" + +struct _ExampleDocumentView +{ + GtkBin parent_instance; + + GtkTextView *text_view; + + ExampleDocument *document; +}; + +enum { + PROP_0, + PROP_DOCUMENT, + N_PROPS +}; + +G_DEFINE_TYPE (ExampleDocumentView, example_document_view, GTK_TYPE_BIN) + +static GParamSpec *properties [N_PROPS]; + +static void +example_document_view_set_document (ExampleDocumentView *self, + ExampleDocument *document) +{ + g_assert (EXAMPLE_IS_DOCUMENT_VIEW (self)); + g_assert (!document || EXAMPLE_IS_DOCUMENT (document)); + + if (g_set_object (&self->document, document)) + { + gtk_text_view_set_buffer (self->text_view, GTK_TEXT_BUFFER (document)); + } +} + +static void +example_document_view_grab_focus (GtkWidget *widget) +{ + ExampleDocumentView *self = EXAMPLE_DOCUMENT_VIEW (widget); + + gtk_widget_grab_focus (GTK_WIDGET (self->text_view)); +} + +static void +example_document_view_finalize (GObject *object) +{ + ExampleDocumentView *self = (ExampleDocumentView *)object; + + g_clear_object (&self->document); + + G_OBJECT_CLASS (example_document_view_parent_class)->finalize (object); +} + +static void +example_document_view_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ExampleDocumentView *self = EXAMPLE_DOCUMENT_VIEW (object); + + switch (prop_id) + { + case PROP_DOCUMENT: + g_value_set_object (value, self->document); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +example_document_view_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ExampleDocumentView *self = EXAMPLE_DOCUMENT_VIEW (object); + + switch (prop_id) + { + case PROP_DOCUMENT: + example_document_view_set_document (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +example_document_view_class_init (ExampleDocumentViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = example_document_view_finalize; + object_class->get_property = example_document_view_get_property; + object_class->set_property = example_document_view_set_property; + + widget_class->grab_focus = example_document_view_grab_focus; + + properties [PROP_DOCUMENT] = + g_param_spec_object ("document", + "Document", + "The document to be viewed", + EXAMPLE_TYPE_DOCUMENT, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/example/ui/example-document-view.ui"); + gtk_widget_class_bind_template_child (widget_class, ExampleDocumentView, text_view); +} + +static void +example_document_view_init (ExampleDocumentView *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget * +example_document_view_new (void) +{ + return g_object_new (EXAMPLE_TYPE_DOCUMENT, NULL); +} diff --git a/examples/app/example-document-view.h b/examples/app/example-document-view.h new file mode 100644 index 0000000..41fa8b0 --- /dev/null +++ b/examples/app/example-document-view.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "example-document.h" + +G_BEGIN_DECLS + +#define EXAMPLE_TYPE_DOCUMENT_VIEW (example_document_view_get_type()) + +G_DECLARE_FINAL_TYPE (ExampleDocumentView, example_document_view, EXAMPLE, DOCUMENT_VIEW, GtkBin) + +GtkWidget *example_document_view_new (void); + +G_END_DECLS diff --git a/examples/app/example-document-view.ui b/examples/app/example-document-view.ui new file mode 100644 index 0000000..fe3ebb3 --- /dev/null +++ b/examples/app/example-document-view.ui @@ -0,0 +1,16 @@ + + + + diff --git a/examples/app/example-document.c b/examples/app/example-document.c new file mode 100644 index 0000000..822de47 --- /dev/null +++ b/examples/app/example-document.c @@ -0,0 +1,109 @@ +#include "example-document.h" + +struct _ExampleDocument +{ + GtkTextBuffer parent_instance; + gchar *title; +}; + +enum { + PROP_0, + PROP_TITLE, + N_PROPS +}; + +G_DEFINE_TYPE (ExampleDocument, example_document, GTK_TYPE_TEXT_BUFFER) + +static guint last_untitled; +static GParamSpec *properties [N_PROPS]; + +static void +example_document_constructed (GObject *object) +{ + ExampleDocument *self = (ExampleDocument *)object; + + if (self->title == NULL) + self->title = g_strdup_printf ("Untitled Document %u", ++last_untitled); + + G_OBJECT_CLASS (example_document_parent_class)->constructed (object); +} + +static void +example_document_finalize (GObject *object) +{ + ExampleDocument *self = (ExampleDocument *)object; + + g_clear_pointer (&self->title, g_free); + + G_OBJECT_CLASS (example_document_parent_class)->finalize (object); +} + +static void +example_document_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ExampleDocument *self = EXAMPLE_DOCUMENT (object); + + switch (prop_id) + { + case PROP_TITLE: + g_value_set_string (value, self->title); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +example_document_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ExampleDocument *self = EXAMPLE_DOCUMENT (object); + + switch (prop_id) + { + case PROP_TITLE: + g_free (self->title); + self->title = g_value_dup_string (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +example_document_class_init (ExampleDocumentClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = example_document_finalize; + object_class->constructed = example_document_constructed; + object_class->get_property = example_document_get_property; + object_class->set_property = example_document_set_property; + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "The title of the document", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +example_document_init (ExampleDocument *self) +{ +} + +ExampleDocument * +example_document_new (void) +{ + return g_object_new (EXAMPLE_TYPE_DOCUMENT, NULL); +} diff --git a/examples/app/example-document.h b/examples/app/example-document.h new file mode 100644 index 0000000..e24815c --- /dev/null +++ b/examples/app/example-document.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define EXAMPLE_TYPE_DOCUMENT (example_document_get_type()) + +G_DECLARE_FINAL_TYPE (ExampleDocument, example_document, EXAMPLE, DOCUMENT, GtkTextBuffer) + +ExampleDocument *example_document_new (void); + +G_END_DECLS diff --git a/examples/app/example-window.c b/examples/app/example-window.c new file mode 100644 index 0000000..267befd --- /dev/null +++ b/examples/app/example-window.c @@ -0,0 +1,200 @@ +#include "config.h" + +#include + +#include "example-window.h" + +struct _ExampleWindow +{ + DzlApplicationWindow parent_instance; + DzlDockBin *dockbin; + GtkHeaderBar *header_bar; + GtkNotebook *notebook; + DzlEmptyState *empty_state; + GtkStack *stack; +}; + +G_DEFINE_TYPE (ExampleWindow, example_window, DZL_TYPE_APPLICATION_WINDOW) + +static const DzlShortcutEntry shortcuts[] = { + { "com.example.window.NewDoc", 0, NULL, N_("Editing"), N_("Documents"), N_("New Document"), N_("Create a new document") }, + { "com.example.window.CloseDoc", 0, NULL, N_("Editing"), N_("Documents"), N_("Close Document"), N_("Close the current document") }, + { "com.example.window.Fullscreen", 0, "F11", N_("Editing"), N_("General"), N_("Fullscreen"), N_("Toggle window fullscreen") }, +}; + +ExampleDocumentView * +example_window_get_current_document_view (ExampleWindow *self) +{ + gint page; + + g_assert (EXAMPLE_IS_WINDOW (self)); + + page = gtk_notebook_get_current_page (self->notebook); + + if (page == -1) + return NULL; + + return EXAMPLE_DOCUMENT_VIEW (gtk_notebook_get_nth_page (self->notebook, page)); +} + +static GtkWidget * +new_label (ExampleDocument *document) +{ + GtkWidget *box; + GtkWidget *label; + GtkWidget *button; + + box = g_object_new (GTK_TYPE_BOX, + "visible", TRUE, + "spacing", 3, + NULL); + label = g_object_new (GTK_TYPE_LABEL, + "xalign", 0.0f, + "visible", TRUE, + NULL); + g_object_bind_property (document, "title", label, "label", G_BINDING_SYNC_CREATE); + button = g_object_new (GTK_TYPE_BUTTON, + "action-name", "win.close-document", + "visible", TRUE, + "child", g_object_new (GTK_TYPE_IMAGE, + "visible", TRUE, + "icon-name", "window-close-symbolic", + "icon-size", GTK_ICON_SIZE_MENU, + NULL), + NULL); + dzl_gtk_widget_add_style_class (button, "flat"); + dzl_gtk_widget_add_style_class (button, "image-button"); + gtk_container_add (GTK_CONTAINER (box), label); + gtk_container_add (GTK_CONTAINER (box), button); + + return box; +} + +static void +new_document_cb (GSimpleAction *action, + GVariant *param, + gpointer user_data) +{ + ExampleWindow *self = user_data; + g_autoptr(ExampleDocument) document = NULL; + GtkWidget *view; + GtkWidget *label; + gint page; + + g_assert (EXAMPLE_IS_WINDOW (self)); + + document = example_document_new (); + view = g_object_new (EXAMPLE_TYPE_DOCUMENT_VIEW, + "visible", TRUE, + "document", document, + NULL); + label = new_label (document); + + page = gtk_notebook_append_page (self->notebook, view, label); + gtk_notebook_set_current_page (self->notebook, page); + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->notebook)); + gtk_widget_grab_focus (view); +} + +static void +close_document_cb (GSimpleAction *action, + GVariant *param, + gpointer user_data) +{ + ExampleWindow *self = user_data; + ExampleDocumentView *view; + + g_assert (EXAMPLE_IS_WINDOW (self)); + + view = example_window_get_current_document_view (self); + + if (view != NULL) + gtk_widget_destroy (GTK_WIDGET (view)); + + if (gtk_notebook_get_n_pages (self->notebook) == 0) + { + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->empty_state)); + gtk_header_bar_set_subtitle (self->header_bar, NULL); + } +} + +static const GActionEntry actions[] = { + { "new-document", new_document_cb }, + { "close-document", close_document_cb }, +}; + +static void +on_page_changed (GtkNotebook *notebook, + GParamSpec *pspec, + ExampleWindow *self) +{ + gint page = gtk_notebook_get_current_page (notebook); + g_autofree gchar *subtitle = NULL; + + if (page >= 0) + { + GtkWidget *view; + g_autoptr(ExampleDocument) document = NULL; + + view = gtk_notebook_get_nth_page (self->notebook, page); + g_object_get (view, "document", &document, NULL); + g_object_get (document, "title", &subtitle, NULL); + } + + gtk_header_bar_set_subtitle (self->header_bar, subtitle); +} + +static void +example_window_class_init (ExampleWindowClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/example/ui/example-window.ui"); + gtk_widget_class_bind_template_child (widget_class, ExampleWindow, dockbin); + gtk_widget_class_bind_template_child (widget_class, ExampleWindow, empty_state); + gtk_widget_class_bind_template_child (widget_class, ExampleWindow, header_bar); + gtk_widget_class_bind_template_child (widget_class, ExampleWindow, notebook); + gtk_widget_class_bind_template_child (widget_class, ExampleWindow, stack); +} + +static void +example_window_init (ExampleWindow *self) +{ + DzlShortcutController *controller; + g_autoptr(GPropertyAction) left = NULL; + g_autoptr(GPropertyAction) right = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + g_signal_connect (self->notebook, + "notify::page", + G_CALLBACK (on_page_changed), + self); + + dzl_shortcut_manager_add_shortcut_entries (NULL, shortcuts, G_N_ELEMENTS (shortcuts), GETTEXT_PACKAGE); + + controller = dzl_shortcut_controller_find (GTK_WIDGET (self)); + + dzl_shortcut_controller_add_command_action (controller, "com.example.window.NewDoc", NULL, 0, "win.new-document"); + dzl_shortcut_controller_add_command_action (controller, "com.example.window.CloseDoc", NULL, 0, "win.close-document"); + dzl_shortcut_controller_add_command_action (controller, "com.example.window.Fullscreen", NULL, 0, "win.fullscreen"); + + g_signal_connect_swapped (self, + "key-press-event", + G_CALLBACK (dzl_shortcut_manager_handle_event), + dzl_shortcut_manager_get_default ()); + + g_action_map_add_action_entries (G_ACTION_MAP (self), actions, G_N_ELEMENTS (actions), self); + + left = g_property_action_new ("show-left-panel", self->dockbin, "left-visible"); + right = g_property_action_new ("show-right-panel", self->dockbin, "right-visible"); + + g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (left)); + g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (right)); +} + +GtkWidget * +example_window_new (void) +{ + return g_object_new (EXAMPLE_TYPE_WINDOW, NULL); +} diff --git a/examples/app/example-window.h b/examples/app/example-window.h new file mode 100644 index 0000000..06507ed --- /dev/null +++ b/examples/app/example-window.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "example-document-view.h" + +G_BEGIN_DECLS + +#define EXAMPLE_TYPE_WINDOW (example_window_get_type()) + +G_DECLARE_FINAL_TYPE (ExampleWindow, example_window, EXAMPLE, WINDOW, DzlApplicationWindow) + +GtkWidget *example_window_new (void); +ExampleDocumentView *example_window_get_current_document_view (ExampleWindow *self); + +G_END_DECLS diff --git a/examples/app/example-window.ui b/examples/app/example-window.ui new file mode 100644 index 0000000..74a33dd --- /dev/null +++ b/examples/app/example-window.ui @@ -0,0 +1,143 @@ + + + + + + diff --git a/examples/app/example.gresources.xml b/examples/app/example.gresources.xml new file mode 100644 index 0000000..5675300 --- /dev/null +++ b/examples/app/example.gresources.xml @@ -0,0 +1,13 @@ + + + + example-document-view.ui + example-window.ui + + + + gtk/menus.ui + shortcuts/default.keytheme + themes/shared.css + + diff --git a/examples/app/gtk/menus.ui b/examples/app/gtk/menus.ui new file mode 100644 index 0000000..2448102 --- /dev/null +++ b/examples/app/gtk/menus.ui @@ -0,0 +1,33 @@ + + + +
+ help-section + Help + + + preferences-desktop-keyboard-shortcuts-symbolic + Keyboard _Shortcuts + app.shortcuts + <control><shift>question + + + _About + app.about + + + _Quit + app.quit + +
+
+ +
+ + _Fullscreen + view-fullscreen-symbolic + win.fullscreen + +
+
+
diff --git a/examples/app/main.c b/examples/app/main.c new file mode 100644 index 0000000..2b00ed9 --- /dev/null +++ b/examples/app/main.c @@ -0,0 +1,17 @@ +#include "example-application.h" + +int +main (int argc, + char *argv[]) +{ + g_autoptr(ExampleApplication) app = NULL; + gint ret; + + app = g_object_new (EXAMPLE_TYPE_APPLICATION, + "application-id", "org.gnome.Example", + "resource-base-path", "/org/gnome/example", + NULL); + ret = g_application_run (G_APPLICATION (app), argc, argv); + + return ret; +} diff --git a/examples/app/meson.build b/examples/app/meson.build new file mode 100644 index 0000000..dca5a96 --- /dev/null +++ b/examples/app/meson.build @@ -0,0 +1,22 @@ +example_resources = gnome.compile_resources( + 'example-resources', + 'example.gresources.xml', + c_name: 'example', +) + +example_application_sources = [ + 'example-application.c', + 'example-application.h', + 'example-document.c', + 'example-document.h', + 'example-document-view.c', + 'example-document-view.h', + 'example-window.c', + 'example-window.h', + 'main.c', + example_resources, +] + +example_application = executable('example-application', example_application_sources, + dependencies: libdazzle_dep, +) diff --git a/examples/app/shortcuts/default.keytheme b/examples/app/shortcuts/default.keytheme new file mode 100644 index 0000000..6ea1e1b --- /dev/null +++ b/examples/app/shortcuts/default.keytheme @@ -0,0 +1,10 @@ + + + Example Shortcuts + + + + + + + diff --git a/examples/app/themes/shared.css b/examples/app/themes/shared.css new file mode 100644 index 0000000..1446e00 --- /dev/null +++ b/examples/app/themes/shared.css @@ -0,0 +1,3 @@ +textview { + padding: 12px; +} diff --git a/examples/graph/js/cpu_graph.js b/examples/graph/js/cpu_graph.js new file mode 100755 index 0000000..8fa8cbf --- /dev/null +++ b/examples/graph/js/cpu_graph.js @@ -0,0 +1,31 @@ +#!/usr/bin/env gjs + +imports.gi.versions["Gdk"] = "3.0" +imports.gi.versions["Gtk"] = "3.0" + +const GLib = imports.gi.GLib; +const Gdk = imports.gi.Gdk; +const Gtk = imports.gi.Gtk; +const Dazzle = imports.gi.Dazzle; + +Gtk.init(null); + +/* + * NOTE: If you are using Dazzle.Application as your base GApplication, + * then the following theme loading is handled for you. You need + * not manually register CSS. + */ +Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), + Dazzle.CssProvider.new ("resource:///org/gnome/dazzle/themes"), + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + +let win = new Gtk.Window({title: "Cpu Graph"}); +let graph = new Dazzle.CpuGraph({timespan: GLib.TIME_SPAN_MINUTE, max_samples: 120}); + +win.add(graph); +win.connect('delete-event', function(){ + Gtk.main_quit(); +}); +win.show_all(); + +Gtk.main(); diff --git a/libdazzle.doap b/libdazzle.doap new file mode 100644 index 0000000..5b7b472 --- /dev/null +++ b/libdazzle.doap @@ -0,0 +1,25 @@ + + + + libdazzle + libdazzle + A library to delight your users with fancy features + libdazzle is a collection of fancy features for GLib and Gtk+ that aren't quite ready or generic enough for use inside those libraries. This is often a proving ground for new widget prototypes. Applications such as Builder tend to drive development of this project. + + + + C + + + + Christian Hergert + + chergert + + + + diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..932cde2 --- /dev/null +++ b/meson.build @@ -0,0 +1,133 @@ +project('libdazzle', 'c', + version: '3.28.5', + license: 'GPLv3+', + meson_version: '>= 0.40.1', + default_options: [ 'warning_level=1', 'buildtype=debugoptimized', 'c_std=gnu11' ], +) + +version_arr = meson.project_version().split('.') +dazzle_version_major = version_arr[0].to_int() +dazzle_version_minor = version_arr[1].to_int() +dazzle_version_micro = version_arr[2].to_int() + +apiversion = '1.0' +soversion = 0 + +if dazzle_version_minor.is_odd() + dazzle_interface_age = 0 +else + dazzle_interface_age = dazzle_version_micro +endif + +# maintaining compatibility with the previous libtool versioning +# current = minor * 100 + micro - interface +# revision = interface +current = dazzle_version_minor * 100 + dazzle_version_micro - dazzle_interface_age +revision = dazzle_interface_age +libversion = '@0@.@1@.@2@'.format(soversion, current, revision) + +config_h = configuration_data() +config_h.set_quoted('GETTEXT_PACKAGE', 'libdazzle') +config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir'))) + +add_project_arguments([ + '-DHAVE_CONFIG_H', + '-DDAZZLE_COMPILATION', + '-I' + meson.build_root(), +], language: 'c') + +root_inc = include_directories('.') +src_inc = include_directories('src') + +cc = meson.get_compiler('c') + +global_c_args = [] +test_c_args = [ + '-Wcast-align', + '-Wdeclaration-after-statement', + '-Wformat-nonliteral', + '-Wformat-security', + '-Wmissing-include-dirs', + '-Wnested-externs', + '-Wno-missing-field-initializers', + '-Wno-sign-compare', + '-Wno-unused-parameter', + '-Wpointer-arith', + '-Wredundant-decls', + '-Wswitch-default', + '-Wswitch-enum', + '-Wuninitialized', + ['-Werror=format-security', '-Werror=format=2' ], + '-Werror=empty-body', + '-Werror=implicit-function-declaration', + #'-Werror=incompatible-pointer-types', + '-Werror=pointer-arith', + '-Werror=init-self', + '-Werror=int-conversion', + '-Werror=misleading-indentation', + '-Werror=missing-include-dirs', + '-Werror=overflow', + '-Werror=parenthesis', + '-Werror=return-type', + '-Werror=shadow', + '-Werror=strict-prototypes', + '-Werror=undef', +] +if get_option('buildtype') != 'plain' + test_c_args += '-fstack-protector-strong' +endif +if get_option('enable_profiling') + test_c_args += '-pg' +endif + +foreach arg: test_c_args + if cc.has_multi_arguments(arg) + global_c_args += arg + endif +endforeach + +# Detect and set symbol visibility +hidden_visibility_args = [] +if get_option('default_library') != 'static' + if host_machine.system() == 'windows' + config_h.set('DLL_EXPORT', true) + if cc.get_id() == 'msvc' + config_h.set('_DZL_EXTERN', '__declspec(dllexport) extern') + elif cc.has_argument('-fvisibility=hidden') + config_h.set('_DZL_EXTERN', '__attribute__((visibility("default"))) __declspec(dllexport) extern') + hidden_visibility_args = ['-fvisibility=hidden'] + endif + elif cc.has_argument('-fvisibility=hidden') + config_h.set('_DZL_EXTERN', '__attribute__((visibility("default"))) extern') + hidden_visibility_args = ['-fvisibility=hidden'] + endif +endif + +add_project_arguments(global_c_args, language: 'c') + +# Setup various paths that subdirectory meson.build files need +package_subdir = get_option('package_subdir') # When used as subproject +libdir = join_paths(get_option('libdir'), package_subdir) +girdir = join_paths(get_option('datadir'), package_subdir, 'gir-1.0') +typelibdir = join_paths(get_option('libdir'), package_subdir, 'girepository-1.0') +if package_subdir != '' + vapidir = join_paths(get_option('datadir'), package_subdir, 'vapi') +else + vapidir = join_paths(get_option('datadir'), 'vala', 'vapi') +endif + +configure_file( + output: 'config.h', + configuration: config_h, +) + +gnome = import('gnome') + +subdir('src') +subdir('tools') +subdir('tests') +subdir('examples/app') + +if get_option('enable_gtk_doc') + subdir('doc') +endif diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..f4232bc --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,27 @@ + +# Performance and debugging related options +option('enable_tracing', type: 'boolean', value: false) +option('enable_profiling', type: 'boolean', value: false) +option('enable_rdtscp', type: 'boolean', value: false, + description: 'Use intel rdtscp haswell instruction for performance counters' +) + +option('enable_tools', type: 'boolean', value: true, + description: 'Whether helper tools should be installed') + +# Support for multiple languages +option('with_introspection', type: 'boolean', value: true) +option('with_vapi', type: 'boolean', value: true) + +# Subproject +option('package_subdir', type: 'string', + description: 'Subdirectory to append to all installed files, for use as subproject' +) + +option('enable_gtk_doc', + type: 'boolean', value: false, + description: 'Whether to generate the API reference for Dazzle') + +option('enable_tests', + type: 'boolean', value: true, + description: 'Whether to compile unit tests') diff --git a/src/Dazzle-1.0.metadata b/src/Dazzle-1.0.metadata new file mode 100644 index 0000000..d450fd8 --- /dev/null +++ b/src/Dazzle-1.0.metadata @@ -0,0 +1,11 @@ +Dazzle name="Dazzle" +* cheader_filename="dazzle.h" +*.*.cancellable#parameter nullable default=null +*.*.io_priority default=GLib.Priority.LOW + +StackList + .push skip=false + .push.create_widget_func nullable +StackListCreateWidgetFunc skip=false + .item type="GLib.Object" + diff --git a/src/actions/dzl-action-group.h b/src/actions/dzl-action-group.h new file mode 100644 index 0000000..7d94067 --- /dev/null +++ b/src/actions/dzl-action-group.h @@ -0,0 +1,210 @@ +/* dzl-action-group.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_ACTION_GROUP_H +#define DZL_ACTION_GROUP_H + +#include + +G_BEGIN_DECLS + +#define DZL_DEFINE_ACTION_GROUP(Type, prefix, ...) \ +struct _##Type##ActionEntry { \ + const gchar *name; \ + void (*activate) (Type *self, GVariant *param); \ + const gchar *parameter_type; \ + const gchar *state; \ + void (*change_state) (Type *self, GVariant *state); \ +} prefix##_actions[] = __VA_ARGS__; \ + \ +typedef struct { \ + GVariant *state; \ + GVariant *state_hint; \ + guint enabled : 1; \ +} Type##ActionInfo; \ + \ +static gboolean \ +_##prefix##_has_action (GActionGroup *group, \ + const gchar *name) \ +{ \ + for (guint i = 0; i < G_N_ELEMENTS(prefix##_actions); i++) \ + { \ + if (g_strcmp0 (name, prefix##_actions[i].name) == 0) \ + return TRUE; \ + } \ + return FALSE; \ +} \ + \ +static gchar ** \ +_##prefix##_list_actions (GActionGroup *group) \ +{ \ + GPtrArray *ar = g_ptr_array_new (); \ + \ + for (guint i = 0; i < G_N_ELEMENTS(prefix##_actions); i++) \ + g_ptr_array_add (ar, g_strdup (prefix##_actions[i].name)); \ + g_ptr_array_add (ar, NULL); \ + \ + return (gchar **)g_ptr_array_free (ar, FALSE); \ +} \ + \ +static void \ +_##prefix##_action_info_free (gpointer data) \ +{ \ + Type##ActionInfo *info = data; \ + g_clear_pointer (&info->state, g_variant_unref); \ + g_clear_pointer (&info->state_hint, g_variant_unref); \ + g_slice_free (Type##ActionInfo, info); \ +} \ + \ +static Type##ActionInfo * \ +_##prefix##_get_action_info (GActionGroup *group, \ + const gchar *name) \ +{ \ + g_autofree gchar *fullname = g_strdup_printf ("ACTION-INFO:%s", name); \ + Type##ActionInfo *info = g_object_get_data (G_OBJECT (group), fullname); \ + if (info == NULL) \ + { \ + info = g_slice_new0 (Type##ActionInfo); \ + info->enabled = TRUE; \ + for (guint i = 0; i < G_N_ELEMENTS(prefix##_actions); i++) \ + { \ + if (g_strcmp0 (prefix##_actions[i].name, name) == 0) \ + { \ + if (prefix##_actions[i].state != NULL) \ + info->state = g_variant_parse ( \ + NULL, prefix##_actions[i].state, NULL, NULL, NULL); \ + break; \ + } \ + } \ + g_object_set_data_full (G_OBJECT (group), fullname, info, \ + _##prefix##_action_info_free); \ + } \ + return info; \ +} \ + \ +static inline void \ +prefix##_set_action_state (Type *self, \ + const gchar *name, \ + GVariant *state) \ +{ \ + Type##ActionInfo *info = _##prefix##_get_action_info (G_ACTION_GROUP (self), \ + name); \ + if (state != info->state) \ + { \ + g_clear_pointer (&info->state, g_variant_unref); \ + info->state = state ? g_variant_ref_sink (state) : NULL; \ + g_action_group_action_state_changed (G_ACTION_GROUP (self), name, state); \ + } \ +} \ + \ +static inline void \ +prefix##_set_action_enabled (Type *self, \ + const gchar *name, \ + gboolean enabled) \ +{ \ + Type##ActionInfo *info = _##prefix##_get_action_info (G_ACTION_GROUP (self), \ + name); \ + if (enabled != info->enabled) \ + { \ + info->enabled = !!enabled; \ + g_action_group_action_enabled_changed (G_ACTION_GROUP (self), \ + name, enabled); \ + } \ +} \ + \ +static void \ +_##prefix##_change_action_state (GActionGroup *group, \ + const gchar *name, \ + GVariant *state) \ +{ \ + for (guint i = 0; i < G_N_ELEMENTS(prefix##_actions); i++) \ + { \ + if (g_strcmp0 (name, prefix##_actions[i].name) == 0) \ + { \ + if (prefix##_actions[i].change_state) \ + prefix##_actions[i].change_state ((Type*)group, state); \ + return; \ + } \ + } \ +} \ + \ +static void \ +_##prefix##_activate_action (GActionGroup *group, \ + const gchar *name, \ + GVariant *param) \ +{ \ + for (guint i = 0; i < G_N_ELEMENTS(prefix##_actions); i++) \ + { \ + if (g_strcmp0 (name, prefix##_actions[i].name) == 0) \ + { \ + if (prefix##_actions[i].activate) \ + prefix##_actions[i].activate ((Type*)group, param); \ + return; \ + } \ + } \ +} \ + \ +static gboolean \ +_##prefix##_query_action (GActionGroup *group, \ + const gchar *name, \ + gboolean *enabled, \ + const GVariantType **parameter_type, \ + const GVariantType **state_type, \ + GVariant **state_hint, \ + GVariant **state) \ +{ \ + for (guint i = 0; i < G_N_ELEMENTS(prefix##_actions); i++) \ + { \ + if (g_strcmp0 (name, prefix##_actions[i].name) == 0) \ + { \ + Type##ActionInfo *info = _##prefix##_get_action_info(group, name); \ + if (prefix##_actions[i].change_state && state_type) \ + *state_type = prefix##_actions[i].parameter_type ? \ + G_VARIANT_TYPE(prefix##_actions[i].parameter_type) : \ + NULL; \ + else if (prefix##_actions[i].activate && parameter_type) \ + *parameter_type = prefix##_actions[i].parameter_type ? \ + G_VARIANT_TYPE(prefix##_actions[i].parameter_type) :\ + NULL; \ + if (state_hint) \ + *state_hint = info->state_hint != NULL ? \ + g_variant_ref (info->state_hint) : NULL; \ + if (state) \ + *state = info->state != NULL ? \ + g_variant_ref (info->state) : NULL; \ + if (enabled) \ + *enabled = info->enabled; \ + return TRUE; \ + } \ + } \ + return FALSE; \ +} \ + \ +static void \ +prefix##_init_action_group (GActionGroupInterface *iface) \ +{ \ + iface->has_action = _##prefix##_has_action; \ + iface->list_actions = _##prefix##_list_actions; \ + iface->change_action_state = _##prefix##_change_action_state; \ + iface->activate_action = _##prefix##_activate_action; \ + iface->query_action = _##prefix##_query_action; \ +} + +G_END_DECLS + +#endif /* DZL_ACTION_GROUP_H */ diff --git a/src/actions/dzl-child-property-action.c b/src/actions/dzl-child-property-action.c new file mode 100644 index 0000000..611034a --- /dev/null +++ b/src/actions/dzl-child-property-action.c @@ -0,0 +1,512 @@ +/* dzl-child-property-action.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-child-property-action" + +#include "config.h" + +#include "dzl-child-property-action.h" +#include "../util/dzl-util-private.h" + +struct _DzlChildPropertyAction +{ + GObject parent_instance; + + GtkContainer *container; + GtkWidget *child; + + const gchar *child_property_name; + const gchar *name; +}; + +enum { + PROP_0, + PROP_CHILD, + PROP_CHILD_PROPERTY_NAME, + PROP_CONTAINER, + N_PROPS, + + PROP_ENABLED, + PROP_NAME, + PROP_PARAMETER_TYPE, + PROP_STATE, + PROP_STATE_TYPE, +}; + +static void action_iface_init (GActionInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (DzlChildPropertyAction, dzl_child_property_action, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_ACTION, action_iface_init)) + +static GParamSpec *properties [N_PROPS]; + +static const gchar * +dzl_child_property_action_get_name (GAction *action) +{ + return DZL_CHILD_PROPERTY_ACTION (action)->name; +} + +static const GVariantType * +dzl_child_property_action_get_state_type (GAction *action) +{ + DzlChildPropertyAction *self = DZL_CHILD_PROPERTY_ACTION (action); + + if (self->container != NULL && + self->child != NULL && + self->child_property_name != NULL) + { + GParamSpec *pspec; + + pspec = gtk_container_class_find_child_property (G_OBJECT_GET_CLASS (self->container), + self->child_property_name); + + if (pspec != NULL) + { + if (G_IS_PARAM_SPEC_BOOLEAN (pspec)) + return G_VARIANT_TYPE ("b"); + else if (G_IS_PARAM_SPEC_INT (pspec)) + return G_VARIANT_TYPE ("i"); + else if (G_IS_PARAM_SPEC_UINT (pspec)) + return G_VARIANT_TYPE ("u"); + else if (G_IS_PARAM_SPEC_STRING (pspec)) + return G_VARIANT_TYPE ("s"); + else if (G_IS_PARAM_SPEC_DOUBLE (pspec)) + return G_VARIANT_TYPE ("d"); + else if (G_IS_PARAM_SPEC_FLOAT (pspec)) + return G_VARIANT_TYPE ("d"); + } + } + + g_warning ("Failed to discover state type for child property %s", + self->child_property_name); + + return NULL; +} + +static const GVariantType * +dzl_child_property_action_get_parameter_type (GAction *action) +{ + const GVariantType *state_type = g_action_get_state_type (action); + + if (g_variant_type_equal (state_type, G_VARIANT_TYPE ("b"))) + return NULL; + + return state_type; +} + + +static GVariant * +dzl_child_property_action_get_state_hint (GAction *action) +{ + return NULL; +} + +static gboolean +dzl_child_property_action_get_enabled (GAction *action) +{ + return TRUE; +} + +static GVariant * +dzl_child_property_action_get_state (GAction *action) +{ + DzlChildPropertyAction *self = DZL_CHILD_PROPERTY_ACTION (action); + + g_assert (DZL_IS_CHILD_PROPERTY_ACTION (self)); + + if (self->container != NULL && + self->child != NULL && + self->child_property_name != NULL) + { + GParamSpec *pspec; + + pspec = gtk_container_class_find_child_property (G_OBJECT_GET_CLASS (self->container), + self->child_property_name); + + if (pspec != NULL) + { + g_auto(GValue) value = G_VALUE_INIT; + GVariant *ret = NULL; + + g_value_init (&value, pspec->value_type); + gtk_container_child_get_property (self->container, + self->child, + self->child_property_name, + &value); + + if (G_IS_PARAM_SPEC_BOOLEAN (pspec)) + ret = g_variant_new_boolean (g_value_get_boolean (&value)); + else if (G_IS_PARAM_SPEC_INT (pspec)) + ret = g_variant_new_int32 (g_value_get_int (&value)); + else if (G_IS_PARAM_SPEC_UINT (pspec)) + ret = g_variant_new_uint32 (g_value_get_uint (&value)); + else if (G_IS_PARAM_SPEC_STRING (pspec)) + ret = g_variant_new_string (g_value_get_string (&value)); + else if (G_IS_PARAM_SPEC_DOUBLE (pspec)) + ret = g_variant_new_double (g_value_get_double (&value)); + else if (G_IS_PARAM_SPEC_FLOAT (pspec)) + ret = g_variant_new_double (g_value_get_double (&value)); + + if (ret) + return g_variant_ref_sink (ret); + } + } + + g_warning ("Failed to determine default state"); + + return NULL; +} + +static void +dzl_child_property_action_change_state (GAction *action, + GVariant *state) +{ + DzlChildPropertyAction *self = DZL_CHILD_PROPERTY_ACTION (action); + + if (self->container != NULL && + self->child != NULL && + self->child_property_name != NULL) + { + GParamSpec *pspec; + + pspec = gtk_container_class_find_child_property (G_OBJECT_GET_CLASS (self->container), + self->child_property_name); + + if (pspec != NULL) + { + g_auto(GValue) value = G_VALUE_INIT; + + g_value_init (&value, pspec->value_type); + + if (G_IS_PARAM_SPEC_BOOLEAN (pspec)) + { + if (!g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN)) + { + g_warning ("Expected 'b', got %s", g_variant_get_type_string (state)); + return; + } + + g_value_set_boolean (&value, g_variant_get_boolean (state)); + } + else if (G_IS_PARAM_SPEC_INT (pspec)) + { + if (!g_variant_is_of_type (state, G_VARIANT_TYPE_INT32)) + { + g_warning ("Expected 'i', got %s", g_variant_get_type_string (state)); + return; + } + + g_value_set_int (&value, g_variant_get_int32 (state)); + } + else if (G_IS_PARAM_SPEC_UINT (pspec)) + { + if (!g_variant_is_of_type (state, G_VARIANT_TYPE_UINT32)) + { + g_warning ("Expected 'u', got %s", g_variant_get_type_string (state)); + return; + } + + g_value_set_uint (&value, g_variant_get_uint32 (state)); + } + else if (G_IS_PARAM_SPEC_STRING (pspec)) + { + if (!g_variant_is_of_type (state, G_VARIANT_TYPE_STRING)) + { + g_warning ("Expected 's', got %s", g_variant_get_type_string (state)); + return; + } + + g_value_set_string (&value, g_variant_get_string (state, NULL)); + } + else if (G_IS_PARAM_SPEC_DOUBLE (pspec) || G_IS_PARAM_SPEC_FLOAT (pspec)) + { + if (!g_variant_is_of_type (state, G_VARIANT_TYPE_STRING)) + { + g_warning ("Expected 'd', got %s", g_variant_get_type_string (state)); + return; + } + + if (G_IS_PARAM_SPEC_DOUBLE (pspec)) + g_value_set_double (&value, g_variant_get_double (state)); + else + g_value_set_float (&value, g_variant_get_double (state)); + } + else + { + g_warning ("I don't know how to handle %s property types.", + g_type_name (pspec->value_type)); + return; + } + + gtk_container_child_set_property (self->container, + self->child, + self->child_property_name, + &value); + g_object_notify (G_OBJECT (self), "state"); + + return; + } + } + + g_warning ("Attempt to change state on incapable child property action"); +} + +static void +dzl_child_property_action_activate (GAction *action, + GVariant *parameter) +{ + DzlChildPropertyAction *self = (DzlChildPropertyAction *)action; + + g_assert (DZL_IS_CHILD_PROPERTY_ACTION (self)); + + if (self->container != NULL && + self->child != NULL && + self->child_property_name != NULL) + { + GParamSpec *pspec; + + pspec = gtk_container_class_find_child_property (G_OBJECT_GET_CLASS (self->container), + self->child_property_name); + + if (pspec != NULL) + { + g_auto(GValue) value = G_VALUE_INIT; + + if (G_IS_PARAM_SPEC_BOOLEAN (pspec)) + { + g_value_init (&value, G_TYPE_BOOLEAN); + + if (parameter != NULL) + g_value_set_boolean (&value, g_variant_get_boolean (parameter)); + else + { + g_auto(GValue) previous = G_VALUE_INIT; + + g_value_init (&previous, G_TYPE_BOOLEAN); + gtk_container_child_get_property (self->container, + self->child, + self->child_property_name, + &previous); + g_value_set_boolean (&value, !g_value_get_boolean (&previous)); + } + } + else if (G_IS_PARAM_SPEC_INT (pspec) && parameter != NULL) + { + g_value_init (&value, G_TYPE_INT); + g_value_set_int (&value, g_variant_get_int32 (parameter)); + } + else if (G_IS_PARAM_SPEC_UINT (pspec) && parameter != NULL) + { + g_value_init (&value, G_TYPE_UINT); + g_value_set_uint (&value, g_variant_get_uint32 (parameter)); + } + else if (G_IS_PARAM_SPEC_STRING (pspec) && parameter != NULL) + { + g_value_init (&value, G_TYPE_STRING); + g_value_set_string (&value, g_variant_get_string (parameter, NULL)); + } + else if (G_IS_PARAM_SPEC_DOUBLE (pspec) || G_IS_PARAM_SPEC_FLOAT (pspec)) + { + if (parameter != NULL) + { + g_value_init (&value, G_TYPE_DOUBLE); + g_value_set_double (&value, g_variant_get_double (parameter)); + } + } + else + { + g_warning ("Failed to transform state type"); + return; + } + + gtk_container_child_set_property (self->container, self->child, pspec->name, &value); + + return; + } + } + + g_warning ("I don't know how to activate %s", self->name); +} + +static void +action_iface_init (GActionInterface *iface) +{ + iface->get_name = dzl_child_property_action_get_name; + iface->get_parameter_type = dzl_child_property_action_get_parameter_type; + iface->get_state_type = dzl_child_property_action_get_state_type; + iface->get_state_hint = dzl_child_property_action_get_state_hint; + iface->get_enabled = dzl_child_property_action_get_enabled; + iface->get_state = dzl_child_property_action_get_state; + iface->change_state = dzl_child_property_action_change_state; + iface->activate = dzl_child_property_action_activate; +} + +static void +child_notify_cb (DzlChildPropertyAction *self, + GParamSpec *pspec, + GtkWidget *child) +{ + g_assert (DZL_IS_CHILD_PROPERTY_ACTION (self)); + g_assert (pspec != NULL); + g_assert (GTK_IS_WIDGET (child)); + + g_object_notify (G_OBJECT (self), "state"); +} + +static void +dzl_child_property_action_dispose (GObject *object) +{ + DzlChildPropertyAction *self = (DzlChildPropertyAction *)object; + + dzl_clear_weak_pointer (&self->container); + dzl_clear_weak_pointer (&self->child); + + G_OBJECT_CLASS (dzl_child_property_action_parent_class)->dispose (object); +} + +static void +dzl_child_property_action_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlChildPropertyAction *self = DZL_CHILD_PROPERTY_ACTION (object); + + switch (prop_id) + { + case PROP_CONTAINER: + g_value_set_object (value, self->container); + break; + + case PROP_CHILD: + g_value_set_object (value, self->child); + break; + + case PROP_CHILD_PROPERTY_NAME: + g_value_set_static_string (value, self->child_property_name); + break; + + case PROP_ENABLED: + g_value_set_boolean (value, dzl_child_property_action_get_enabled (G_ACTION (self))); + break; + + case PROP_PARAMETER_TYPE: + g_value_set_boxed (value, dzl_child_property_action_get_parameter_type (G_ACTION (self))); + break; + + case PROP_STATE: + g_value_take_variant (value, dzl_child_property_action_get_state (G_ACTION (self))); + break; + + case PROP_STATE_TYPE: + g_value_set_boxed (value, dzl_child_property_action_get_state_type (G_ACTION (self))); + break; + + case PROP_NAME: + g_value_set_static_string (value, self->name); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_child_property_action_class_init (DzlChildPropertyActionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = dzl_child_property_action_dispose; + object_class->get_property = dzl_child_property_action_get_property; + + g_object_class_override_property (object_class, PROP_ENABLED, "enabled"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PARAMETER_TYPE, "parameter-type"); + g_object_class_override_property (object_class, PROP_STATE, "state"); + g_object_class_override_property (object_class, PROP_STATE_TYPE, "state-type"); + + properties [PROP_CHILD] = + g_param_spec_object ("child", + "Child", + "The child widget", + GTK_TYPE_WIDGET, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_CHILD_PROPERTY_NAME] = + g_param_spec_string ("child-property-name", + "Child Property Name", + "The name of the child property", + NULL, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_CONTAINER] = + g_param_spec_object ("container", + "Container", + "The container widget", + GTK_TYPE_CONTAINER, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_child_property_action_init (DzlChildPropertyAction *self) +{ +} + +/** + * dzl_child_property_action_new: + * @name: the name of the action + * @container: the container of the widget + * @child: the widget for the child property + * @child_property_name: the name of the child property + * + * This creates a new #GAction that will change when the underlying child + * property of @container changes for @child. + * + * Returns: (transfer full): A new #DzlChildPropertyAction. + */ +GAction * +dzl_child_property_action_new (const gchar *name, + GtkContainer *container, + GtkWidget *child, + const gchar *child_property_name) +{ + g_autoptr(DzlChildPropertyAction) self = NULL; + g_autofree gchar *signal_detail = NULL; + + g_return_val_if_fail (GTK_IS_CONTAINER (container), NULL); + g_return_val_if_fail (GTK_IS_WIDGET (child), NULL); + g_return_val_if_fail (child_property_name != NULL, NULL); + + self = g_object_new (DZL_TYPE_CHILD_PROPERTY_ACTION, NULL); + self->name = g_intern_string (name); + self->child_property_name = g_intern_string (child_property_name); + dzl_set_weak_pointer (&self->container, container); + dzl_set_weak_pointer (&self->child, child); + + signal_detail = g_strdup_printf ("child-notify::%s", child_property_name); + + g_signal_connect_object (child, + signal_detail, + G_CALLBACK (child_notify_cb), + self, + G_CONNECT_SWAPPED); + + return G_ACTION (g_steal_pointer (&self)); +} diff --git a/src/actions/dzl-child-property-action.h b/src/actions/dzl-child-property-action.h new file mode 100644 index 0000000..0425269 --- /dev/null +++ b/src/actions/dzl-child-property-action.h @@ -0,0 +1,45 @@ +/* dzl-child-property-action.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_CHILD_PROPERTY_ACTION_H +#define DZL_CHILD_PROPERTY_ACTION_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_CHILD_PROPERTY_ACTION (dzl_child_property_action_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlChildPropertyAction, dzl_child_property_action, DZL, CHILD_PROPERTY_ACTION, GObject) + +DZL_AVAILABLE_IN_ALL +GAction *dzl_child_property_action_new (const gchar *name, + GtkContainer *container, + GtkWidget *child, + const gchar *child_property_name); + +G_END_DECLS + +#endif /* DZL_CHILD_PROPERTY_ACTION_H */ diff --git a/src/actions/dzl-properties-group.c b/src/actions/dzl-properties-group.c new file mode 100644 index 0000000..8b59bee --- /dev/null +++ b/src/actions/dzl-properties-group.c @@ -0,0 +1,1004 @@ +/* dzl-properties-group.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-properties-group" + +#include "config.h" + +#include "dzl-properties-group.h" + +/** + * SECTION:dzl-properties-group + * @title: DzlPropertiesGroup + * @short_description: A #GActionGroup of properties on an object + * + * This class is a #GActionGroup which provides stateful access to + * properties in a #GObject. This can be useful when you want to + * expose properties from a GObject as a #GAction, espectially with + * use in GtkApplications. + * + * Call dzl_properties_group_add_property() to setup the mappings + * for action-name to property-name for the actions you'd like to + * add. + * + * Not all property types can be supported. What is current supported + * are properties of type: + * + * %G_TYPE_INT + * %G_TYPE_UINT + * %G_TYPE_BOOLEAN + * %G_TYPE_STRING + * %G_TYPE_DOUBLE + * + * Since: 3.26 + * + * Since 3.28 enums are supported by using their enum value nick as + * a string. + */ + +struct _DzlPropertiesGroup +{ + GObject parent_instance; + + /* + * All subsequent set_object() calls must be this type. + */ + GType prerequisite; + + /* + * Weak ref to the object we are monitoring for property changes. + * We hold both a GWeakRef and a g_object_weak_ref() on the object + * so that we can get notified of destruction *AND* know when we + * can safely weak_unref() without invalid pointer user. + */ + GWeakRef object_ref; + + /* + * Since the list of mappings are fairly small, we just choose to + * use an array of all mappings rather than two-hashtables to map + * from action-name -> property-name and vice versa. Element type + * is of struct Mapping. + * + * The strings in the mapping are intern'd to allow for direct + * pointer comparison with GParamSpec information. + */ + GArray *mappings; +}; + +typedef struct +{ + const gchar *action_name; + const GVariantType *param_type; + const GVariantType *state_type; + const gchar *property_name; + GType property_type; + DzlPropertiesFlags flags : 8; + guint can_read : 1; + guint can_write : 1; +} Mapping; + +enum { + PROP_0, + PROP_OBJECT, + PROP_OBJECT_TYPE, + N_PROPS +}; + +static GVariant * +get_action_state (GObject *object, + const Mapping *mapping) +{ + g_auto(GValue) value = G_VALUE_INIT; + GVariant *ret = NULL; + + g_assert (G_IS_OBJECT (object)); + g_assert (mapping != NULL); + + if (!mapping->can_read) + return NULL; + + g_value_init (&value, mapping->property_type); + g_object_get_property (object, mapping->property_name, &value); + + switch (mapping->property_type) + { + case G_TYPE_INT: + ret = g_variant_new_int32 (g_value_get_int (&value)); + break; + + case G_TYPE_UINT: + ret = g_variant_new_uint32 (g_value_get_uint (&value)); + break; + + case G_TYPE_DOUBLE: + ret = g_variant_new_double (g_value_get_double (&value)); + break; + + case G_TYPE_STRING: + if (!g_value_get_string (&value)) + ret = g_variant_new_string (""); + else + ret = g_variant_new_string (g_value_get_string (&value)); + break; + + case G_TYPE_BOOLEAN: + ret = g_variant_new_boolean (g_value_get_boolean (&value)); + break; + + default: + if (g_type_is_a (mapping->property_type, G_TYPE_ENUM)) + { + GEnumClass *eclass = g_type_class_ref (mapping->property_type); + GEnumValue *eval = g_enum_get_value (eclass, g_value_get_enum (&value)); + + if (eval != NULL) + ret = g_variant_new_string (eval->value_nick); + + g_clear_pointer (&eclass, g_type_class_unref); + + break; + } + + g_assert_not_reached (); + } + + return g_variant_take_ref (ret); +} + +static gboolean +dzl_properties_group_query_action (GActionGroup *group, + const gchar *action_name, + gboolean *enabled, + const GVariantType **param_type, + const GVariantType **state_type, + GVariant **state_hint, + GVariant **state) +{ + DzlPropertiesGroup *self = (DzlPropertiesGroup *)group; + + g_assert (DZL_IS_PROPERTIES_GROUP (self)); + g_assert (action_name != NULL); + + for (guint i = 0; i < self->mappings->len; i++) + { + const Mapping *mapping = &g_array_index (self->mappings, Mapping, i); + + if (g_strcmp0 (mapping->action_name, action_name) == 0) + { + g_autoptr(GObject) object = g_weak_ref_get (&self->object_ref); + + if (enabled) + *enabled = (object != NULL); + + if (param_type) + *param_type = mapping->param_type; + + if (state_type) + *state_type = mapping->state_type; + + if (state_hint) + *state_hint = NULL; + + if (state) + { + if (object) + *state = get_action_state (object, mapping); + else + *state = NULL; + } + + return TRUE; + } + } + + return FALSE; +} + +static gchar ** +dzl_properties_group_list_actions (GActionGroup *group) +{ + DzlPropertiesGroup *self = (DzlPropertiesGroup *)group; + GPtrArray *ar; + + g_assert (DZL_IS_PROPERTIES_GROUP (self)); + + ar = g_ptr_array_new (); + + for (guint i = 0; i < self->mappings->len; i++) + { + const Mapping *mapping = &g_array_index (self->mappings, Mapping, i); + + g_ptr_array_add (ar, g_strdup (mapping->action_name)); + } + + g_ptr_array_add (ar, NULL); + + return (gchar **)g_ptr_array_free (ar, FALSE); +} + +static gboolean +dzl_properties_group_has_action (GActionGroup *group, + const gchar *name) +{ + DzlPropertiesGroup *self = (DzlPropertiesGroup *)group; + + g_assert (DZL_IS_PROPERTIES_GROUP (self)); + g_assert (name != NULL); + + for (guint i = 0; i < self->mappings->len; i++) + { + const Mapping *mapping = &g_array_index (self->mappings, Mapping, i); + + if (g_strcmp0 (name, mapping->action_name) == 0) + return TRUE; + } + + return FALSE; +} + +static gboolean +dzl_properties_group_get_action_enabled (GActionGroup *group, + const gchar *name) +{ + DzlPropertiesGroup *self = (DzlPropertiesGroup *)group; + g_autoptr(GObject) object = NULL; + + g_assert (DZL_IS_PROPERTIES_GROUP (self)); + g_assert (name != NULL); + + object = g_weak_ref_get (&self->object_ref); + + return (object != NULL); +} + +static const GVariantType * +dzl_properties_group_get_action_parameter_type (GActionGroup *group, + const gchar *name) +{ + DzlPropertiesGroup *self = (DzlPropertiesGroup *)group; + + g_assert (DZL_IS_PROPERTIES_GROUP (self)); + g_assert (name != NULL); + + for (guint i = 0; i < self->mappings->len; i++) + { + const Mapping *mapping = &g_array_index (self->mappings, Mapping, i); + + if (g_strcmp0 (name, mapping->action_name) == 0) + { + /* Normal parameter type for boolean actions is NULL. But if + * we are treating them statefully, where the new state is the + * activation state, then handle that here. + */ + if (mapping->property_type == G_TYPE_BOOLEAN && + (mapping->flags & DZL_PROPERTIES_FLAGS_STATEFUL_BOOLEANS) != 0) + return G_VARIANT_TYPE_BOOLEAN; + + return mapping->param_type; + } + } + + return NULL; +} + +static const GVariantType * +dzl_properties_group_get_action_state_type (GActionGroup *group, + const gchar *name) +{ + DzlPropertiesGroup *self = (DzlPropertiesGroup *)group; + + g_assert (DZL_IS_PROPERTIES_GROUP (self)); + g_assert (name != NULL); + + for (guint i = 0; i < self->mappings->len; i++) + { + const Mapping *mapping = &g_array_index (self->mappings, Mapping, i); + + if (g_strcmp0 (name, mapping->action_name) == 0) + return mapping->state_type; + } + + return NULL; +} + +static GVariant * +dzl_properties_group_get_action_state_hint (GActionGroup *group, + const gchar *name) +{ + g_assert (DZL_IS_PROPERTIES_GROUP (group)); + g_assert (name != NULL); + + return NULL; +} + +static void +dzl_properties_group_change_action_state (GActionGroup *group, + const gchar *name, + GVariant *variant) +{ + DzlPropertiesGroup *self = (DzlPropertiesGroup *)group; + g_autoptr(GObject) object = NULL; + const GVariantType *expected; + + g_assert (DZL_IS_PROPERTIES_GROUP (self)); + g_assert (name != NULL); + + expected = dzl_properties_group_get_action_state_type (group, name); + + if (variant == NULL || !g_variant_is_of_type (variant, expected)) + { + g_warning ("Invalid state for action \"%s\". Expected %s.", + name, (const gchar *)expected); + return; + } + + object = g_weak_ref_get (&self->object_ref); + + if (object == NULL) + { + g_warning ("Attempt to change state of %s after action was disabled", + name); + return; + } + + for (guint i = 0; i < self->mappings->len; i++) + { + const Mapping *mapping = &g_array_index (self->mappings, Mapping, i); + + if (g_strcmp0 (name, mapping->action_name) == 0) + { + g_auto(GValue) value = G_VALUE_INIT; + + if (!mapping->can_write) + { + g_warning ("property is not writable, ignoring request to change state"); + break; + } + + switch (mapping->property_type) + { + case G_TYPE_INT: + g_value_init (&value, G_TYPE_INT); + g_value_set_int (&value, g_variant_get_int32 (variant)); + break; + + case G_TYPE_UINT: + g_value_init (&value, G_TYPE_UINT); + g_value_set_uint (&value, g_variant_get_uint32 (variant)); + break; + + case G_TYPE_BOOLEAN: + g_value_init (&value, G_TYPE_BOOLEAN); + g_value_set_boolean (&value, g_variant_get_boolean (variant)); + break; + + case G_TYPE_STRING: + g_value_init (&value, G_TYPE_STRING); + /* No need to dup the string, its lifetime is longer */ + g_value_set_static_string (&value, g_variant_get_string (variant, NULL)); + break; + + case G_TYPE_DOUBLE: + g_value_init (&value, G_TYPE_DOUBLE); + g_value_set_double (&value, g_variant_get_double (variant)); + break; + + default: + if (g_type_is_a (mapping->property_type, G_TYPE_ENUM)) + { + const gchar *str = g_variant_get_string (variant, NULL); + GEnumClass *eclass = g_type_class_ref (mapping->property_type); + + if (eclass != NULL) + { + GEnumValue *eval = g_enum_get_value_by_nick (eclass, str); + + if (eval != NULL) + { + g_value_init (&value, mapping->property_type); + g_value_set_enum (&value, eval->value); + g_clear_pointer (&eclass, g_type_class_unref); + break; + } + } + + g_clear_pointer (&eclass, g_type_class_unref); + g_warning ("Failed to transform '%s' to %s", + str, g_type_name (mapping->property_type)); + return; + } + + g_assert_not_reached (); + } + + g_object_set_property (object, mapping->property_name, &value); + + break; + } + } +} + +static void +dzl_properties_group_activate_action (GActionGroup *group, + const gchar *name, + GVariant *variant) +{ + DzlPropertiesGroup *self = (DzlPropertiesGroup *)group; + g_autoptr(GObject) object = NULL; + + g_assert (DZL_IS_PROPERTIES_GROUP (self)); + g_assert (name != NULL); + + object = g_weak_ref_get (&self->object_ref); + + if (object == NULL) + { + g_warning ("Attempt to activate %s after action was disabled", name); + return; + } + + for (guint i = 0; i < self->mappings->len; i++) + { + const Mapping *mapping = &g_array_index (self->mappings, Mapping, i); + + if (g_strcmp0 (name, mapping->action_name) == 0) + { + if (mapping->property_type == G_TYPE_BOOLEAN && + (mapping->flags & DZL_PROPERTIES_FLAGS_STATEFUL_BOOLEANS) == 0) + { + gboolean value = FALSE; + + g_object_get (object, mapping->property_name, &value, NULL); + value = !value; + g_object_set (object, mapping->property_name, value, NULL); + } + else + { + dzl_properties_group_change_action_state (group, name, variant); + } + + break; + } + } +} + +static void +action_group_iface_init (GActionGroupInterface *iface) +{ + iface->has_action = dzl_properties_group_has_action; + iface->list_actions = dzl_properties_group_list_actions; + iface->get_action_enabled = dzl_properties_group_get_action_enabled; + iface->get_action_parameter_type = dzl_properties_group_get_action_parameter_type; + iface->get_action_state_type = dzl_properties_group_get_action_state_type; + iface->get_action_state_hint = dzl_properties_group_get_action_state_hint; + iface->change_action_state = dzl_properties_group_change_action_state; + iface->activate_action = dzl_properties_group_activate_action; + iface->query_action = dzl_properties_group_query_action; +} + +G_DEFINE_TYPE_WITH_CODE (DzlPropertiesGroup, dzl_properties_group, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, action_group_iface_init)) + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_properties_group_notify (DzlPropertiesGroup *self, + GParamSpec *pspec, + GObject *object) +{ + g_assert (DZL_IS_PROPERTIES_GROUP (self)); + g_assert (pspec != NULL); + g_assert (G_IS_OBJECT (object)); + + /* mappings is generally quite small, so iterating the array + * is going to have similar performance to a hashtable lookup + * plus pointer chaseing. + */ + + for (guint i = 0; i < self->mappings->len; i++) + { + const Mapping *mapping = &g_array_index (self->mappings, Mapping, i); + + if (mapping->property_name == pspec->name) + { + g_autoptr(GVariant) state = get_action_state (object, mapping); + g_action_group_action_state_changed (G_ACTION_GROUP (self), + mapping->action_name, + state); + break; + } + } +} + +static const GVariantType * +get_param_type_for_type (GType type, + DzlPropertiesFlags flags) +{ + switch (type) + { + case G_TYPE_INT: return G_VARIANT_TYPE_INT32; + case G_TYPE_UINT: return G_VARIANT_TYPE_UINT32; + case G_TYPE_STRING: return G_VARIANT_TYPE_STRING; + case G_TYPE_DOUBLE: return G_VARIANT_TYPE_DOUBLE; + + case G_TYPE_BOOLEAN: + if (flags & DZL_PROPERTIES_FLAGS_STATEFUL_BOOLEANS) + return G_VARIANT_TYPE_BOOLEAN; + return NULL; + + default: + if (g_type_is_a (type, G_TYPE_ENUM)) + return G_VARIANT_TYPE_STRING; + + g_warning ("%s is not a supported type", g_type_name (type)); + return NULL; + } +} + +static const GVariantType * +get_state_type_for_type (GType type) +{ + switch (type) + { + case G_TYPE_INT: return G_VARIANT_TYPE_INT32; + case G_TYPE_UINT: return G_VARIANT_TYPE_UINT32; + case G_TYPE_BOOLEAN: return G_VARIANT_TYPE_BOOLEAN; + case G_TYPE_STRING: return G_VARIANT_TYPE_STRING; + case G_TYPE_DOUBLE: return G_VARIANT_TYPE_DOUBLE; + + default: + if (g_type_is_a (type, G_TYPE_ENUM)) + return G_VARIANT_TYPE_STRING; + + g_warning ("%s is not a supported type", g_type_name (type)); + return NULL; + } +} + +static void +dzl_properties_group_notify_all_disabled (DzlPropertiesGroup *self) +{ + g_assert (DZL_IS_PROPERTIES_GROUP (self)); + + for (guint i = 0; i < self->mappings->len; i++) + { + const Mapping *mapping = &g_array_index (self->mappings, Mapping, i); + g_action_group_action_enabled_changed (G_ACTION_GROUP (self), + mapping->action_name, + FALSE); + } +} + +static void +dzl_properties_group_weak_notify (gpointer data, + GObject *where_object_was) +{ + DzlPropertiesGroup *self = data; + + g_assert (DZL_IS_PROPERTIES_GROUP (self)); + + g_weak_ref_set (&self->object_ref, NULL); + + dzl_properties_group_notify_all_disabled (self); +} + +static void +dzl_properties_group_set_object (DzlPropertiesGroup *self, + GObject *object) +{ + g_autoptr(GObject) old_object = NULL; + + g_assert (DZL_IS_PROPERTIES_GROUP (self)); + g_assert (!object || G_IS_OBJECT (object)); + + old_object = g_weak_ref_get (&self->object_ref); + + /* Nothing to do if we aren't changing anything */ + if (object == old_object) + return; + + if (self->prerequisite == G_TYPE_INVALID && object != NULL) + self->prerequisite = G_OBJECT_TYPE (object); + + /* Disconnect previous life-cycle tracking */ + if (old_object != NULL) + { + g_signal_handlers_disconnect_by_func (old_object, + G_CALLBACK (dzl_properties_group_notify), + self); + g_object_weak_unref (old_object, + dzl_properties_group_weak_notify, + self); + g_weak_ref_set (&self->object_ref, NULL); + } + + /* Mark all actions as disabled if we lost our object */ + if (object == NULL) + { + dzl_properties_group_notify_all_disabled (self); + return; + } + + g_signal_connect_object (object, + "notify", + G_CALLBACK (dzl_properties_group_notify), + self, + G_CONNECT_SWAPPED); + + /* WeakRef so we can detect 3-rd degree disposal */ + g_weak_ref_set (&self->object_ref, object); + + /* Weak notify so we can get notified of the case */ + g_object_weak_ref (G_OBJECT (object), + dzl_properties_group_weak_notify, + self); + + /* Emit state changes for all properties */ + for (guint i = 0; i < self->mappings->len; i++) + { + const Mapping *mapping = &g_array_index (self->mappings, Mapping, i); + g_autoptr(GVariant) state = get_action_state (object, mapping); + + g_action_group_action_state_changed (G_ACTION_GROUP (self), + mapping->action_name, + state); + } +} + +static void +dzl_properties_group_finalize (GObject *object) +{ + DzlPropertiesGroup *self = (DzlPropertiesGroup *)object; + g_autoptr(GObject) weak_obj = NULL; + + weak_obj = g_weak_ref_get (&self->object_ref); + + if (weak_obj != NULL) + { + /* + * No need to disconnect signal handler as we are in finalize and + * g_signal_connect_object() tracks this for us. + */ + g_object_weak_unref (weak_obj, + dzl_properties_group_weak_notify, + self); + } + + g_weak_ref_clear (&self->object_ref); + + g_clear_pointer (&self->mappings, g_array_unref); + + G_OBJECT_CLASS (dzl_properties_group_parent_class)->finalize (object); +} + +static void +dzl_properties_group_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPropertiesGroup *self = DZL_PROPERTIES_GROUP (object); + + switch (prop_id) + { + case PROP_OBJECT: + g_value_take_object (value, g_weak_ref_get (&self->object_ref)); + break; + + case PROP_OBJECT_TYPE: + g_value_set_gtype (value, self->prerequisite); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_properties_group_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPropertiesGroup *self = DZL_PROPERTIES_GROUP (object); + + switch (prop_id) + { + case PROP_OBJECT: + dzl_properties_group_set_object (self, g_value_get_object (value)); + break; + + case PROP_OBJECT_TYPE: + if (g_value_get_gtype (value) != G_TYPE_INVALID && + g_value_get_gtype (value) != G_TYPE_OBJECT) + self->prerequisite = g_value_get_gtype (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_properties_group_class_init (DzlPropertiesGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_properties_group_finalize; + object_class->get_property = dzl_properties_group_get_property; + object_class->set_property = dzl_properties_group_set_property; + + properties [PROP_OBJECT] = + g_param_spec_object ("object", + "Object", + "The source object for the properties", + G_TYPE_OBJECT, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_OBJECT_TYPE] = + g_param_spec_gtype ("object-type", + "Object Type", + "A type the object must conform to.", + G_TYPE_OBJECT, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_properties_group_init (DzlPropertiesGroup *self) +{ + g_weak_ref_init (&self->object_ref, NULL); + + self->mappings = g_array_new (FALSE, FALSE, sizeof (Mapping)); +} + +/** + * dzl_properties_group_new: + * @object: The object containing the properties + * + * This creates a new #DzlPropertiesGroup to create stateful actions + * around properties in @object. + * + * Call dzl_properties_group_add_property() to add a property to + * action name mapping for this group. Until you've called this, + * no actions are mapped. + * + * Note that #DzlPropertiesGroup only holds a weak reference to + * @object and therefore you must keep @object alive elsewhere. + * + * Returns: (transfer full): A #DzlPropertiesGroup + * + * Since: 3.26 + */ +DzlPropertiesGroup * +dzl_properties_group_new (GObject *object) +{ + g_return_val_if_fail (G_IS_OBJECT (object), NULL); + + return g_object_new (DZL_TYPE_PROPERTIES_GROUP, + "object", object, + "object-type", G_OBJECT_TYPE (object), + NULL); +} + +/** + * dzl_properties_group_add_property_full: + * @self: a #DzlPropertiesGroup + * @name: the name of the action + * @property_name: the name of the property + * @flags: optional flags for the action + * + * Adds a new stateful action named @name which maps to the underlying + * property @property_name of #DzlPropertiesGroup:object. + * + * Seting @flags allows you to tweak some settings about the action. + * + * Since: 3.26 + */ +void +dzl_properties_group_add_property_full (DzlPropertiesGroup *self, + const gchar *name, + const gchar *property_name, + DzlPropertiesFlags flags) +{ + GObjectClass *object_class = NULL; + GParamSpec *pspec; + Mapping mapping = { 0 }; + + g_return_if_fail (DZL_IS_PROPERTIES_GROUP (self)); + g_return_if_fail (name != NULL); + g_return_if_fail (property_name != NULL); + + if (self->prerequisite == G_TYPE_INVALID) + { + g_warning ("Cannot add properties before object has been set."); + return; + } + + object_class = g_type_class_ref (self->prerequisite); + + if (object_class == NULL || !G_IS_OBJECT_CLASS (object_class)) + { + g_warning ("Implausable result for prerequisite, not a GObjectClass"); + goto failure; + } + + pspec = g_object_class_find_property (object_class, property_name); + + if (pspec == NULL) + { + g_warning ("No such property \"%s\" on type %s", + property_name, G_OBJECT_CLASS_NAME (object_class)); + goto failure; + } + + mapping.action_name = g_intern_string (name); + mapping.param_type = get_param_type_for_type (pspec->value_type, flags); + mapping.state_type = get_state_type_for_type (pspec->value_type); + mapping.property_name = pspec->name; + mapping.property_type = pspec->value_type; + mapping.flags = flags; + mapping.can_read = !!(pspec->flags & G_PARAM_READABLE); + mapping.can_write = !!(pspec->flags & G_PARAM_WRITABLE); + + /* we already warned, ignore this */ + if (mapping.state_type == NULL) + goto failure; + + g_array_append_val (self->mappings, mapping); + + g_action_group_action_added (G_ACTION_GROUP (self), mapping.action_name); + +failure: + g_clear_pointer (&object_class, g_type_class_unref); +} + +/** + * dzl_properties_group_add_property: + * @self: a #DzlPropertiesGroup + * @name: the name of the action + * @property_name: the name of the property + * + * Adds a new stateful action named @name which maps to the underlying + * property @property_name of #DzlPropertiesGroup:object. + * + * Since: 3.26 + */ +void +dzl_properties_group_add_property (DzlPropertiesGroup *self, + const gchar *name, + const gchar *property_name) +{ + dzl_properties_group_add_property_full (self, name, property_name, 0); +} + +/** + * dzl_properties_group_remove: + * @self: a #DzlPropertiesGroup + * @name: the name of the action + * + * Removes an action from @self that was previously added with + * dzl_properties_group_add_property(). @name should match the + * name parameter to that function. + * + * Since: 3.26 + */ +void +dzl_properties_group_remove (DzlPropertiesGroup *self, + const gchar *name) +{ + g_return_if_fail (DZL_IS_PROPERTIES_GROUP (self)); + g_return_if_fail (name != NULL); + + name = g_intern_string (name); + + for (guint i = 0; i < self->mappings->len; i++) + { + const Mapping *mapping = &g_array_index (self->mappings, Mapping, i); + + if (mapping->action_name == name) + { + g_array_remove_index_fast (self->mappings, i); + g_action_group_action_removed (G_ACTION_GROUP (self), name); + break; + } + } +} + +/** + * dzl_properties_group_add_all_properties: + * @self: A #DzlPropertiesGroup + * + * This function will try to add all properties found on the target + * instance to the group. Only properties that are supported by the + * #DzlPropertiesGroup will be added. + * + * The action name of all added properties will be identical to their + * property name. + * + * Since: 3.26 + */ +void +dzl_properties_group_add_all_properties (DzlPropertiesGroup *self) +{ + g_autofree GParamSpec **pspec = NULL; + GObjectClass *object_class = NULL; + guint n_pspec = 0; + + g_return_if_fail (DZL_IS_PROPERTIES_GROUP (self)); + + if (self->prerequisite == G_TYPE_INVALID) + { + g_warning ("Cannot add properties, no object has been set"); + return; + } + + object_class = g_type_class_ref (self->prerequisite); + + if (object_class == NULL || !G_IS_OBJECT_CLASS (object_class)) + { + g_warning ("Implausable result, not a GObjectClass"); + goto failure; + } + + pspec = g_object_class_list_properties (object_class, &n_pspec); + + for (guint i = 0; i < n_pspec; i++) + { + switch (pspec[i]->value_type) + { + case G_TYPE_BOOLEAN: + case G_TYPE_DOUBLE: + case G_TYPE_INT: + case G_TYPE_STRING: + case G_TYPE_UINT: + dzl_properties_group_add_property (self, pspec[i]->name, pspec[i]->name); + break; + + default: + if (g_type_is_a (pspec[i]->value_type, G_TYPE_ENUM)) + dzl_properties_group_add_property (self, pspec[i]->name, pspec[i]->name); + break; + } + } + +failure: + g_clear_pointer (&object_class, g_type_class_unref); +} + +/** + * dzl_properties_group_new_for_type: + * @object_type: A #GObjectClass based type + * + * This creates a new #DzlPropertiesGroup for which the initial object is + * %NULL. + * + * Set @object_type to a type of a class which is a #GObject-based type. + * + * Returns: (transfer none): A #DzlPropertiesGroup. + */ +DzlPropertiesGroup * +dzl_properties_group_new_for_type (GType object_type) +{ + g_return_val_if_fail (g_type_is_a (object_type, G_TYPE_OBJECT), NULL); + + return g_object_new (DZL_TYPE_PROPERTIES_GROUP, + "object-type", object_type, + NULL); +} diff --git a/src/actions/dzl-properties-group.h b/src/actions/dzl-properties-group.h new file mode 100644 index 0000000..19959c9 --- /dev/null +++ b/src/actions/dzl-properties-group.h @@ -0,0 +1,60 @@ +/* dzl-properties-group.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PROPERTIES_GROUP_H +#define DZL_PROPERTIES_GROUP_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PROPERTIES_GROUP (dzl_properties_group_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlPropertiesGroup, dzl_properties_group, DZL, PROPERTIES_GROUP, GObject) + +typedef enum +{ + DZL_PROPERTIES_FLAGS_NONE, + DZL_PROPERTIES_FLAGS_STATEFUL_BOOLEANS = 1 << 0, +} DzlPropertiesFlags; + +DZL_AVAILABLE_IN_ALL +DzlPropertiesGroup *dzl_properties_group_new (GObject *object); +DZL_AVAILABLE_IN_ALL +DzlPropertiesGroup *dzl_properties_group_new_for_type (GType object_type); +DZL_AVAILABLE_IN_ALL +void dzl_properties_group_add_property (DzlPropertiesGroup *self, + const gchar *name, + const gchar *property_name); +DZL_AVAILABLE_IN_ALL +void dzl_properties_group_add_property_full (DzlPropertiesGroup *self, + const gchar *name, + const gchar *property_name, + DzlPropertiesFlags flags); +DZL_AVAILABLE_IN_ALL +void dzl_properties_group_add_all_properties (DzlPropertiesGroup *self); +DZL_AVAILABLE_IN_ALL +void dzl_properties_group_remove (DzlPropertiesGroup *self, + const gchar *name); + +G_END_DECLS + +#endif /* DZL_PROPERTIES_GROUP_H */ diff --git a/src/actions/dzl-settings-flag-action.c b/src/actions/dzl-settings-flag-action.c new file mode 100644 index 0000000..9a0bd0e --- /dev/null +++ b/src/actions/dzl-settings-flag-action.c @@ -0,0 +1,315 @@ +/* dzl-settings-flag-action.c + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-settings-flag-action" + +#include "config.h" + +#include "dzl-settings-flag-action.h" + +struct _DzlSettingsFlagAction +{ + GObject parent_instance; + + GSettings *settings; + + gchar *schema_id; + gchar *schema_key; + gchar *flag_nick; + gchar *name; +}; + +static void action_iface_init (GActionInterface *iface); + +G_DEFINE_TYPE_EXTENDED (DzlSettingsFlagAction, dzl_settings_flag_action, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE (G_TYPE_ACTION, action_iface_init)) + +enum { + PROP_0, + PROP_SCHEMA_ID, + PROP_SCHEMA_KEY, + PROP_FLAG_NICK, + LAST_PROP, + + PROP_ENABLED, + PROP_NAME, + PROP_STATE, + PROP_STATE_TYPE, + PROP_PARAMETER_TYPE, +}; + +static GParamSpec *properties [LAST_PROP]; + +/** + * dzl_settings_flag_action_new: + * + * This creates a new action that can be used to toggle an individual flag in + * a #GSettings key which is of a flags type. + * + * Returns: (transfer full): A new #GAction. + */ +GAction * +dzl_settings_flag_action_new (const gchar *schema_id, + const gchar *schema_key, + const gchar *flag_nick) +{ + return g_object_new (DZL_TYPE_SETTINGS_FLAG_ACTION, + "schema-id", schema_id, + "schema-key", schema_key, + "flag-nick", flag_nick, + NULL); +} + +static void +dzl_settings_flag_action_finalize (GObject *object) +{ + DzlSettingsFlagAction *self = (DzlSettingsFlagAction *)object; + + g_clear_pointer (&self->schema_id, g_free); + g_clear_pointer (&self->schema_key, g_free); + g_clear_pointer (&self->flag_nick, g_free); + + G_OBJECT_CLASS (dzl_settings_flag_action_parent_class)->finalize (object); +} + +static void +dzl_settings_flag_action_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSettingsFlagAction *self = DZL_SETTINGS_FLAG_ACTION (object); + + switch (prop_id) + { + case PROP_ENABLED: + g_value_set_boolean (value, self->schema_id != NULL); + break; + + case PROP_SCHEMA_ID: + g_value_set_string (value, self->schema_id); + break; + + case PROP_NAME: + g_value_set_string (value, g_action_get_name (G_ACTION (self))); + break; + + case PROP_SCHEMA_KEY: + g_value_set_string (value, self->schema_key); + break; + + case PROP_FLAG_NICK: + g_value_set_string (value, self->flag_nick); + break; + + case PROP_STATE: + case PROP_STATE_TYPE: + case PROP_PARAMETER_TYPE: + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_settings_flag_action_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSettingsFlagAction *self = DZL_SETTINGS_FLAG_ACTION (object); + + switch (prop_id) + { + case PROP_SCHEMA_ID: + g_free (self->schema_id); + self->schema_id = g_value_dup_string (value); + break; + + case PROP_SCHEMA_KEY: + g_free (self->schema_key); + self->schema_key = g_value_dup_string (value); + break; + + case PROP_FLAG_NICK: + g_free (self->flag_nick); + self->flag_nick = g_value_dup_string (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_settings_flag_action_class_init (DzlSettingsFlagActionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_settings_flag_action_finalize; + object_class->get_property = dzl_settings_flag_action_get_property; + object_class->set_property = dzl_settings_flag_action_set_property; + + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_STATE, "state"); + g_object_class_override_property (object_class, PROP_STATE_TYPE, "state-type"); + g_object_class_override_property (object_class, PROP_PARAMETER_TYPE, "parameter-type"); + g_object_class_override_property (object_class, PROP_ENABLED, "enabled"); + + properties [PROP_SCHEMA_ID] = + g_param_spec_string ("schema-id", + "Schema Id", + "Schema Id", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SCHEMA_KEY] = + g_param_spec_string ("schema-key", + "Schema Key", + "Schema Key", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_FLAG_NICK] = + g_param_spec_string ("flag-nick", + "Flag Nick", + "Flag Nick", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_settings_flag_action_init (DzlSettingsFlagAction *self) +{ +} + +static GSettings * +dzl_settings_flag_action_get_settings (DzlSettingsFlagAction *self) +{ + g_assert (DZL_IS_SETTINGS_FLAG_ACTION (self)); + + if (self->settings == NULL) + self->settings = g_settings_new (self->schema_id); + + return self->settings; +} + +static const gchar * +dzl_settings_flag_action_get_name (GAction *action) +{ + DzlSettingsFlagAction *self = (DzlSettingsFlagAction *)action; + + if (self->name == NULL) + self->name = g_strdup_printf ("%s-%s", self->schema_key, self->flag_nick); + + return self->name; +} + +static const GVariantType * +dzl_settings_flag_action_get_parameter_type (GAction *action) +{ + return NULL; +} + +static const GVariantType * +dzl_settings_flag_action_get_state_type (GAction *action) +{ + return G_VARIANT_TYPE_BOOLEAN; +} + +static GVariant * +dzl_settings_flag_action_get_state_hint (GAction *action) +{ + return NULL; +} + +static void +dzl_settings_flag_action_change_state (GAction *action, + GVariant *value) +{ +} + +static GVariant * +dzl_settings_flag_action_get_state (GAction *action) +{ + DzlSettingsFlagAction *self = (DzlSettingsFlagAction *)action; + GSettings *settings = dzl_settings_flag_action_get_settings (self); + g_auto(GStrv) flags = g_settings_get_strv (settings, self->schema_key); + gboolean state = g_strv_contains ((const gchar * const *)flags, self->flag_nick); + return g_variant_new_boolean (state); +} + +static gboolean +dzl_settings_flag_action_get_enabled (GAction *action) +{ + DzlSettingsFlagAction *self = (DzlSettingsFlagAction *)action; + + return self->schema_id && self->schema_key && self->flag_nick; +} + +static void +dzl_settings_flag_action_activate (GAction *action, + GVariant *parameter) +{ + DzlSettingsFlagAction *self = (DzlSettingsFlagAction *)action; + GSettings *settings; + GPtrArray *ar; + gboolean found = FALSE; + gchar **strv; + guint i; + + g_assert (DZL_IS_SETTINGS_FLAG_ACTION (action)); + g_assert (parameter == NULL); + + settings = dzl_settings_flag_action_get_settings (self); + strv = g_settings_get_strv (settings, self->schema_key); + ar = g_ptr_array_new (); + + for (i = 0; strv [i]; i++) + { + if (g_strcmp0 (strv [i], self->flag_nick) == 0) + found = TRUE; + else + g_ptr_array_add (ar, strv [i]); + } + + if (!found) + g_ptr_array_add (ar, self->flag_nick); + + g_ptr_array_add (ar, NULL); + + g_settings_set_strv (settings, self->schema_key, (const gchar * const *)ar->pdata); + + g_strfreev (strv); +} + +static void +action_iface_init (GActionInterface *iface) +{ + iface->activate = dzl_settings_flag_action_activate; + iface->change_state = dzl_settings_flag_action_change_state; + iface->get_enabled = dzl_settings_flag_action_get_enabled; + iface->get_name = dzl_settings_flag_action_get_name; + iface->get_parameter_type = dzl_settings_flag_action_get_parameter_type; + iface->get_state = dzl_settings_flag_action_get_state; + iface->get_state_hint = dzl_settings_flag_action_get_state_hint; + iface->get_state_type = dzl_settings_flag_action_get_state_type; +} diff --git a/src/actions/dzl-settings-flag-action.h b/src/actions/dzl-settings-flag-action.h new file mode 100644 index 0000000..4edbda7 --- /dev/null +++ b/src/actions/dzl-settings-flag-action.h @@ -0,0 +1,40 @@ +/* dzl-settings-flag-action.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_SETTINGS_FLAG_ACTION_H +#define DZL_SETTINGS_FLAG_ACTION_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SETTINGS_FLAG_ACTION (dzl_settings_flag_action_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlSettingsFlagAction, dzl_settings_flag_action, DZL, SETTINGS_FLAG_ACTION, GObject) + +DZL_AVAILABLE_IN_ALL +GAction *dzl_settings_flag_action_new (const gchar *schema_id, + const gchar *schema_key, + const gchar *flag_nick); + +G_END_DECLS + +#endif /* DZL_SETTINGS_FLAG_ACTION_H */ diff --git a/src/actions/dzl-widget-action-group.c b/src/actions/dzl-widget-action-group.c new file mode 100644 index 0000000..e5ea5e6 --- /dev/null +++ b/src/actions/dzl-widget-action-group.c @@ -0,0 +1,652 @@ +/* dzl-widget-action-group.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-widget-action-group" + +#include "config.h" + +#include + +#include "dzl-widget-action-group.h" + +struct _DzlWidgetActionGroup +{ + GObject parent_instance; + GtkWidget *widget; + GHashTable *enabled; +}; + +static void action_group_iface_init (GActionGroupInterface *iface); + +G_DEFINE_TYPE_EXTENDED (DzlWidgetActionGroup, dzl_widget_action_group, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, action_group_iface_init)) + +enum { + PROP_0, + PROP_WIDGET, + N_PROPS +}; + +static GHashTable *cached_types; +static GParamSpec *properties [N_PROPS]; + +static gboolean +supports_types (const GType *types, + guint n_types) +{ + guint i; + + g_assert (types != NULL || n_types == 0); + + for (i = 0; i < n_types; i++) + { + switch (types [i]) + { + case G_TYPE_STRING: + case G_TYPE_INT: + case G_TYPE_UINT: + case G_TYPE_INT64: + case G_TYPE_UINT64: + case G_TYPE_BOOLEAN: + case G_TYPE_DOUBLE: + case G_TYPE_FLOAT: + case G_TYPE_CHAR: + case G_TYPE_UCHAR: + case G_TYPE_ENUM: + case G_TYPE_FLAGS: + case G_TYPE_VARIANT: + case G_TYPE_NONE: + break; + + default: + if (G_TYPE_IS_FLAGS (types [i]) || G_TYPE_IS_ENUM (types [i])) + break; + + return FALSE; + } + } + + return TRUE; +} + +static const GVariantType * +create_variant_type (const GType *types, + guint n_types) +{ + const GVariantType *ret = NULL; + GString *str; + + g_assert (types != NULL || n_types == 0); + + str = g_string_new ("("); + + for (guint i = 0; i < n_types; i++) + { + switch (types [i]) + { + case G_TYPE_STRING: + g_string_append_c (str, 's'); + break; + + case G_TYPE_INT: + g_string_append_c (str, 'i'); + break; + + case G_TYPE_UINT: + g_string_append_c (str, 'u'); + break; + + case G_TYPE_INT64: + g_string_append_c (str, 'x'); + break; + + case G_TYPE_UINT64: + g_string_append_c (str, 't'); + break; + + case G_TYPE_BOOLEAN: + g_string_append_c (str, 'b'); + break; + + case G_TYPE_DOUBLE: + case G_TYPE_FLOAT: + g_string_append_c (str, 'd'); + break; + + case G_TYPE_CHAR: + case G_TYPE_UCHAR: + g_string_append_c (str, 'y'); + break; + + case G_TYPE_VARIANT: + g_string_append_c (str, 'v'); + break; + + case G_TYPE_NONE: + break; + + default: + if (G_TYPE_IS_ENUM (types [i]) || G_TYPE_IS_FLAGS (types [i])) + { + g_string_append_c (str, 'u'); + break; + } + + g_string_free (str, TRUE); + return NULL; + } + } + + g_string_append_c (str, ')'); + + if (g_str_equal (str->str, "()")) + { + g_string_free (str, TRUE); + return NULL; + } + + if (cached_types == NULL) + cached_types = g_hash_table_new (g_str_hash, g_str_equal); + + ret = g_hash_table_lookup (cached_types, str->str); + + if (ret == NULL) + { + gchar *type_str = g_string_free (str, FALSE); + g_hash_table_insert (cached_types, type_str, type_str); + ret = (const GVariantType *)type_str; + } + else + g_string_free (str, TRUE); + + return ret; +} + +static void +do_activate (DzlWidgetActionGroup *self, + GtkWidget *widget, + GSignalQuery *query, + GVariant *params) +{ + g_auto(GValue) return_value = G_VALUE_INIT; + g_auto(GValue) instance = G_VALUE_INIT; + GArray *ar; + GVariantIter iter; + gsize n_children; + + g_assert (query != NULL); + g_assert (GTK_IS_WIDGET (widget)); + + if (params != NULL) + g_debug ("Activating %s with %s\n", query->signal_name, g_variant_print (params, TRUE)); + + if (params == NULL && query->n_params != 0) + { + g_critical ("%s::%s() requires %d parameters", + G_OBJECT_TYPE_NAME (widget), query->signal_name, query->n_params); + return; + } + + if (query->return_type != G_TYPE_NONE) + g_value_init (&return_value, query->return_type); + + g_value_init (&instance, query->itype); + g_value_set_object (&instance, widget); + + if (params == NULL) + { + g_signal_emitv (&instance, query->signal_id, 0, &return_value); + return; + } + + g_assert (g_variant_is_container (params)); + g_assert (params != NULL); + + n_children = g_variant_iter_init (&iter, params); + + if (n_children != query->n_params) + { + g_critical ("%s::%s() requires %d params, got %d", + G_OBJECT_TYPE_NAME (widget), query->signal_name, + (gint)n_children, query->n_params); + return; + } + + ar = g_array_new (FALSE, FALSE, sizeof (GValue)); + + g_array_append_val (ar, instance); + + g_variant_iter_init (&iter, params); + + for (guint i = 0; i < query->n_params; i++) + { + g_autoptr(GVariant) param = NULL; + GValue value = G_VALUE_INIT; + + param = g_variant_iter_next_value (&iter); + +#define CONVERT_PARAM(TYPE, VARIANT_TYPE, setter, getter, ...) \ + case G_TYPE_##TYPE: \ + { \ + if (!g_variant_is_of_type (param, G_VARIANT_TYPE_##VARIANT_TYPE)) \ + { \ + g_critical ("parameter type mismatch for signal %s", \ + query->signal_name); \ + goto skip_emit; \ + } \ + g_value_init (&value, G_TYPE_##TYPE); \ + g_value_set_##setter (&value, g_variant_get_##getter (param, ##__VA_ARGS__)); \ + g_array_append_val (ar, value); \ + } \ + break + + switch (query->param_types [i]) + { + CONVERT_PARAM(STRING, STRING, string, string, NULL); + CONVERT_PARAM(INT, INT32, int, int32); + CONVERT_PARAM(UINT, UINT32, uint, uint32); + CONVERT_PARAM(INT64, INT64, int64, int64); + CONVERT_PARAM(UINT64, UINT64, uint64, uint64); + CONVERT_PARAM(BOOLEAN, BOOLEAN, boolean, boolean); + CONVERT_PARAM(DOUBLE, DOUBLE, double, double); + CONVERT_PARAM(FLOAT, DOUBLE, float, double); + CONVERT_PARAM(CHAR, BYTE, schar, byte); + CONVERT_PARAM(UCHAR, BYTE, uchar, byte); + CONVERT_PARAM(VARIANT, VARIANT, variant, variant); + + default: + if (G_TYPE_IS_ENUM(query->param_types [i])) + { + if (!g_variant_is_of_type (param, G_VARIANT_TYPE_UINT32)) + goto skip_emit; + g_value_init (&value, query->param_types [i]); + g_value_set_enum (&value, g_variant_get_uint32 (param)); + g_array_append_val (ar, value); + break; + } + else if (G_TYPE_IS_FLAGS (query->param_types [i])) + { + if (!g_variant_is_of_type (param, G_VARIANT_TYPE_UINT32)) + goto skip_emit; + g_value_init (&value, query->param_types [i]); + g_value_set_flags (&value, g_variant_get_uint32 (param)); + g_array_append_val (ar, value); + break; + } + + g_critical ("Unknown param type: %s", g_type_name (query->param_types [i])); + goto skip_emit; + } + +#undef CONVERT_PARAM + } + + g_signal_emitv ((GValue *)(gpointer)ar->data, query->signal_id, 0, &return_value); + +skip_emit: + /* ignore instance */ + for (guint i = 1; i < ar->len; i++) + g_value_unset (&g_array_index (ar, GValue, i)); + + g_array_unref (ar); +} + +static void +dzl_widget_action_group_set_widget (DzlWidgetActionGroup *self, + GtkWidget *widget) +{ + g_assert (DZL_IS_WIDGET_ACTION_GROUP (self)); + g_assert (!widget || GTK_IS_WIDGET (widget)); + + if (widget != self->widget) + { + if (self->widget != NULL) + { + g_signal_handlers_disconnect_by_func (self->widget, + G_CALLBACK (gtk_widget_destroyed), + &self->widget); + self->widget = NULL; + } + + if (widget != NULL) + { + self->widget = widget; + g_signal_connect (self->widget, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &self->widget); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WIDGET]); + } +} + +static void +dzl_widget_action_group_finalize (GObject *object) +{ + DzlWidgetActionGroup *self = (DzlWidgetActionGroup *)object; + + g_clear_pointer (&self->enabled, g_hash_table_unref); + + G_OBJECT_CLASS (dzl_widget_action_group_parent_class)->finalize (object); +} + +static void +dzl_widget_action_group_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlWidgetActionGroup *self = DZL_WIDGET_ACTION_GROUP (object); + + switch (prop_id) + { + case PROP_WIDGET: + g_value_set_object (value, self->widget); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_widget_action_group_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlWidgetActionGroup *self = DZL_WIDGET_ACTION_GROUP (object); + + switch (prop_id) + { + case PROP_WIDGET: + dzl_widget_action_group_set_widget (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_widget_action_group_class_init (DzlWidgetActionGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_widget_action_group_finalize; + object_class->get_property = dzl_widget_action_group_get_property; + object_class->set_property = dzl_widget_action_group_set_property; + + properties [PROP_WIDGET] = + g_param_spec_object ("widget", + "Widget", + "Widget", + GTK_TYPE_WIDGET, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_widget_action_group_init (DzlWidgetActionGroup *self) +{ +} + +static gboolean +dzl_widget_action_group_has_action (GActionGroup *group, + const gchar *action_name) +{ + DzlWidgetActionGroup *self = (DzlWidgetActionGroup *)group; + + g_assert (DZL_IS_WIDGET_ACTION_GROUP (self)); + g_assert (action_name != NULL); + + if (GTK_IS_WIDGET (self->widget)) + return (0 != g_signal_lookup (action_name, G_OBJECT_TYPE (self->widget))); + + return FALSE; +} + +static gchar ** +dzl_widget_action_group_list_actions (GActionGroup *group) +{ + DzlWidgetActionGroup *self = (DzlWidgetActionGroup *)group; + GPtrArray *ar; + + g_assert (DZL_IS_WIDGET_ACTION_GROUP (self)); + + ar = g_ptr_array_new (); + + if (self->widget != NULL) + { + for (GType type = G_OBJECT_TYPE (self->widget); + type != G_TYPE_INVALID; + type = g_type_parent (type)) + { + g_autofree guint *signal_ids = NULL; + guint n_ids = 0; + guint i; + + signal_ids = g_signal_list_ids (type, &n_ids); + + for (i = 0; i < n_ids; i++) + { + GSignalQuery query; + + g_signal_query (signal_ids[i], &query); + + if ((query.signal_flags & G_SIGNAL_ACTION) != 0) + g_ptr_array_add (ar, g_strdup (query.signal_name)); + } + } + } + + g_ptr_array_add (ar, NULL); + + return (gchar **)g_ptr_array_free (ar, FALSE); +} + +static gboolean +dzl_widget_action_group_get_action_enabled (GActionGroup *group, + const gchar *action_name) +{ + DzlWidgetActionGroup *self = (DzlWidgetActionGroup *)group; + + g_assert (DZL_IS_WIDGET_ACTION_GROUP (group)); + g_assert (action_name != NULL); + + if (self->enabled && g_hash_table_contains (self->enabled, action_name)) + return GPOINTER_TO_INT (g_hash_table_lookup (self->enabled, action_name)); + + return TRUE; +} + +const GVariantType * +dzl_widget_action_group_get_action_parameter_type (GActionGroup *group, + const gchar *action_name) +{ + DzlWidgetActionGroup *self = (DzlWidgetActionGroup *)group; + GSignalQuery query; + guint signal_id; + + g_assert (DZL_IS_WIDGET_ACTION_GROUP (self)); + g_assert (action_name != NULL); + + if (!GTK_IS_WIDGET (self->widget)) + return NULL; + + signal_id = g_signal_lookup (action_name, G_OBJECT_TYPE (self->widget)); + if (signal_id == 0) + return NULL; + + g_signal_query (signal_id, &query); + + if (!supports_types (query.param_types, query.n_params)) + return NULL; + + return create_variant_type (query.param_types, query.n_params); +} + +const GVariantType * +dzl_widget_action_group_get_action_state_type (GActionGroup *group, + const gchar *action_name) +{ + g_assert (DZL_IS_WIDGET_ACTION_GROUP (group)); + g_assert (action_name != NULL); + + return NULL; +} + +static void +dzl_widget_action_group_activate_action (GActionGroup *group, + const gchar *action_name, + GVariant *params) +{ + DzlWidgetActionGroup *self = (DzlWidgetActionGroup *)group; + + g_assert (DZL_IS_WIDGET_ACTION_GROUP (group)); + g_assert (action_name != NULL); + + if (GTK_IS_WIDGET (self->widget)) + { + guint signal_id; + + signal_id = g_signal_lookup (action_name, G_OBJECT_TYPE (self->widget)); + + if (signal_id != 0) + { + GSignalQuery query; + + g_signal_query (signal_id, &query); + + if (query.signal_flags & G_SIGNAL_ACTION) + { + do_activate (self, self->widget, &query, params); + return; + } + } + } + + g_warning ("Failed to activate action %s due to missing widget or action", + action_name); +} + +static gboolean +dzl_widget_action_group_query_action (GActionGroup *group, + const gchar *action_name, + gboolean *enabled, + const GVariantType **parameter_type, + const GVariantType **state_type, + GVariant **state_hint, + GVariant **state) +{ + DzlWidgetActionGroup *self = (DzlWidgetActionGroup *)group; + + g_assert (DZL_IS_WIDGET_ACTION_GROUP (group)); + + if (!GTK_IS_WIDGET (self->widget)) + return FALSE; + + if (!g_signal_lookup (action_name, G_OBJECT_TYPE (self->widget))) + return FALSE; + + if (state_hint) + *state_hint = NULL; + + if (state_type) + *state_type = NULL; + + if (state) + *state = NULL; + + if (parameter_type) + *parameter_type = dzl_widget_action_group_get_action_parameter_type (group, action_name); + + if (enabled) + *enabled = dzl_widget_action_group_get_action_enabled (group, action_name); + + return TRUE; +} + +static void +action_group_iface_init (GActionGroupInterface *iface) +{ + iface->has_action = dzl_widget_action_group_has_action; + iface->list_actions = dzl_widget_action_group_list_actions; + iface->get_action_enabled = dzl_widget_action_group_get_action_enabled; + iface->get_action_parameter_type = dzl_widget_action_group_get_action_parameter_type; + iface->get_action_state_type = dzl_widget_action_group_get_action_state_type; + iface->activate_action = dzl_widget_action_group_activate_action; + iface->query_action = dzl_widget_action_group_query_action; +} + +/** + * dzl_widget_action_group_new: + * + * Returns: (transfer full): An #DzlWidgetActionGroup. + */ +GActionGroup * +dzl_widget_action_group_new (GtkWidget *widget) +{ + return g_object_new (DZL_TYPE_WIDGET_ACTION_GROUP, + "widget", widget, + NULL); +} + +/** + * dzl_widget_action_group_attach: + * @widget: (type Gtk.Widget): A #GtkWidget + * @group_name: the group name to use for the action group + * + * Helper function to create an #DzlWidgetActionGroup and attach + * it to @widget using the group name @group_name. + */ +void +dzl_widget_action_group_attach (gpointer widget, + const gchar *group_name) +{ + GActionGroup *group; + + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (group_name != NULL); + + group = dzl_widget_action_group_new (widget); + gtk_widget_insert_action_group (widget, group_name, group); + g_object_unref (group); +} + +void +dzl_widget_action_group_set_action_enabled (DzlWidgetActionGroup *self, + const gchar *action_name, + gboolean enabled) +{ + g_return_if_fail (DZL_IS_WIDGET_ACTION_GROUP (self)); + g_return_if_fail (action_name != NULL); + g_return_if_fail (dzl_widget_action_group_has_action (G_ACTION_GROUP (self), action_name)); + + enabled = !!enabled; + + if (self->enabled == NULL) + self->enabled = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + g_hash_table_insert (self->enabled, g_strdup (action_name), GINT_TO_POINTER (enabled)); + g_action_group_action_enabled_changed (G_ACTION_GROUP (self), action_name, enabled); + + g_debug ("Action %s %s", action_name, enabled ? "enabled" : "disabled"); +} diff --git a/src/actions/dzl-widget-action-group.h b/src/actions/dzl-widget-action-group.h new file mode 100644 index 0000000..5c92623 --- /dev/null +++ b/src/actions/dzl-widget-action-group.h @@ -0,0 +1,45 @@ +/* dzl-widget-action-group.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_WIDGET_ACTION_GROUP_H +#define DZL_WIDGET_ACTION_GROUP_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_WIDGET_ACTION_GROUP (dzl_widget_action_group_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlWidgetActionGroup, dzl_widget_action_group, DZL, WIDGET_ACTION_GROUP, GObject) + +DZL_AVAILABLE_IN_ALL +GActionGroup *dzl_widget_action_group_new (GtkWidget *widget); +DZL_AVAILABLE_IN_ALL +void dzl_widget_action_group_attach (gpointer widget, + const gchar *group_name); +DZL_AVAILABLE_IN_ALL +void dzl_widget_action_group_set_action_enabled (DzlWidgetActionGroup *self, + const gchar *action_name, + gboolean enabled); + +G_END_DECLS + +#endif /* DZL_WIDGET_ACTION_GROUP_H */ diff --git a/src/actions/meson.build b/src/actions/meson.build new file mode 100644 index 0000000..def436d --- /dev/null +++ b/src/actions/meson.build @@ -0,0 +1,19 @@ +action_headers = [ + 'dzl-action-group.h', + 'dzl-child-property-action.h', + 'dzl-properties-group.h', + 'dzl-settings-flag-action.h', + 'dzl-widget-action-group.h', +] + +action_sources = [ + 'dzl-child-property-action.c', + 'dzl-properties-group.c', + 'dzl-settings-flag-action.c', + 'dzl-widget-action-group.c', +] + +libdazzle_public_headers += files(action_headers) +libdazzle_public_sources += files(action_sources) + +install_headers(action_headers, subdir: join_paths(libdazzle_header_subdir, 'actions')) diff --git a/src/animation/dzl-animation.c b/src/animation/dzl-animation.c new file mode 100644 index 0000000..1727ef4 --- /dev/null +++ b/src/animation/dzl-animation.c @@ -0,0 +1,1265 @@ +/* dzl-animation.c + * + * Copyright (C) 2010-2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-animation" + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include "dzl-animation.h" +#include "dzl-frame-source.h" + +#define FALLBACK_FRAME_RATE 60 + +typedef gdouble (*AlphaFunc) (gdouble offset); +typedef void (*TweenFunc) (const GValue *begin, + const GValue *end, + GValue *value, + gdouble offset); + +typedef struct +{ + gboolean is_child; /* Does GParamSpec belong to parent widget */ + GParamSpec *pspec; /* GParamSpec of target property */ + GValue begin; /* Begin value in animation */ + GValue end; /* End value in animation */ +} Tween; + + +struct _DzlAnimation +{ + GInitiallyUnowned parent_instance; + + gpointer target; /* Target object to animate */ + gint64 begin_time; /* Time in which animation started */ + gint64 end_time; /* Deadline for the animation */ + guint duration_msec; /* Duration in milliseconds */ + guint mode; /* Tween mode */ + gulong tween_handler; /* GSource or signal handler */ + gulong after_paint_handler; /* signal handler */ + gdouble last_offset; /* Track our last offset */ + GArray *tweens; /* Array of tweens to perform */ + GdkFrameClock *frame_clock; /* An optional frame-clock for sync. */ + GDestroyNotify notify; /* Notify callback */ + gpointer notify_data; /* Data for notify */ +}; + +G_DEFINE_TYPE (DzlAnimation, dzl_animation, G_TYPE_INITIALLY_UNOWNED) + +enum { + PROP_0, + PROP_DURATION, + PROP_FRAME_CLOCK, + PROP_MODE, + PROP_TARGET, + LAST_PROP +}; + + +enum { + TICK, + LAST_SIGNAL +}; + + +/* + * Helper macros. + */ +#define LAST_FUNDAMENTAL 64 +#define TWEEN(type) \ + static void \ + tween_ ## type (const GValue * begin, \ + const GValue * end, \ + GValue * value, \ + gdouble offset) \ + { \ + g ## type x = g_value_get_ ## type (begin); \ + g ## type y = g_value_get_ ## type (end); \ + g_value_set_ ## type (value, x + ((y - x) * offset)); \ + } + + +/* + * Globals. + */ +static AlphaFunc alpha_funcs[DZL_ANIMATION_LAST]; +static gboolean debug; +static GParamSpec *properties[LAST_PROP]; +static guint signals[LAST_SIGNAL]; +static TweenFunc tween_funcs[LAST_FUNDAMENTAL]; +static guint slow_down_factor = 1; + + +/* + * Tweeners for basic types. + */ +TWEEN (int); +TWEEN (uint); +TWEEN (long); +TWEEN (ulong); +TWEEN (float); +TWEEN (double); + + +/** + * dzl_animation_alpha_ease_in_cubic: + * @offset: (in): The position within the animation; 0.0 to 1.0. + * + * An alpha function to transform the offset within the animation. + * @DZL_ANIMATION_CUBIC means the valu ewill be transformed into + * cubic acceleration (x * x * x). + */ +static gdouble +dzl_animation_alpha_ease_in_cubic (gdouble offset) +{ + return offset * offset * offset; +} + + +static gdouble +dzl_animation_alpha_ease_out_cubic (gdouble offset) +{ + gdouble p = offset - 1.0; + + return p * p * p + 1.0; +} + +static gdouble +dzl_animation_alpha_ease_in_out_cubic (gdouble offset) +{ + gdouble p = offset * 2.0; + + if (p < 1.0) + return 0.5 * p * p * p; + p -= 2.0; + return 0.5 * (p * p * p + 2.0); +} + + +/** + * dzl_animation_alpha_linear: + * @offset: (in): The position within the animation; 0.0 to 1.0. + * + * An alpha function to transform the offset within the animation. + * @DZL_ANIMATION_LINEAR means no tranformation will be made. + * + * Returns: @offset. + * Side effects: None. + */ +static gdouble +dzl_animation_alpha_linear (gdouble offset) +{ + return offset; +} + + +/** + * dzl_animation_alpha_ease_in_quad: + * @offset: (in): The position within the animation; 0.0 to 1.0. + * + * An alpha function to transform the offset within the animation. + * @DZL_ANIMATION_EASE_IN_QUAD means that the value will be transformed + * into a quadratic acceleration. + * + * Returns: A tranformation of @offset. + * Side effects: None. + */ +static gdouble +dzl_animation_alpha_ease_in_quad (gdouble offset) +{ + return offset * offset; +} + + +/** + * dzl_animation_alpha_ease_out_quad: + * @offset: (in): The position within the animation; 0.0 to 1.0. + * + * An alpha function to transform the offset within the animation. + * @DZL_ANIMATION_EASE_OUT_QUAD means that the value will be transformed + * into a quadratic deceleration. + * + * Returns: A tranformation of @offset. + * Side effects: None. + */ +static gdouble +dzl_animation_alpha_ease_out_quad (gdouble offset) +{ + return -1.0 * offset * (offset - 2.0); +} + + +/** + * dzl_animation_alpha_ease_in_out_quad: + * @offset: (in): The position within the animation; 0.0 to 1.0. + * + * An alpha function to transform the offset within the animation. + * @DZL_ANIMATION_EASE_IN_OUT_QUAD means that the value will be transformed + * into a quadratic acceleration for the first half, and quadratic + * deceleration the second half. + * + * Returns: A tranformation of @offset. + * Side effects: None. + */ +static gdouble +dzl_animation_alpha_ease_in_out_quad (gdouble offset) +{ + offset *= 2.0; + if (offset < 1.0) + return 0.5 * offset * offset; + offset -= 1.0; + return -0.5 * (offset * (offset - 2.0) - 1.0); +} + + +/** + * dzl_animation_load_begin_values: + * @animation: (in): A #DzlAnimation. + * + * Load the begin values for all the properties we are about to + * animate. + * + * Side effects: None. + */ +static void +dzl_animation_load_begin_values (DzlAnimation *animation) +{ + GtkContainer *container; + Tween *tween; + guint i; + + g_assert (DZL_IS_ANIMATION (animation)); + + for (i = 0; i < animation->tweens->len; i++) + { + tween = &g_array_index (animation->tweens, Tween, i); + g_value_reset (&tween->begin); + if (tween->is_child) + { + container = GTK_CONTAINER (gtk_widget_get_parent (animation->target)); + gtk_container_child_get_property (container, + animation->target, + tween->pspec->name, + &tween->begin); + } + else + { + g_object_get_property (animation->target, + tween->pspec->name, + &tween->begin); + } + } +} + + +/** + * dzl_animation_unload_begin_values: + * @animation: (in): A #DzlAnimation. + * + * Unloads the begin values for the animation. This might be particularly + * useful once we support pointer types. + * + * Side effects: None. + */ +static void +dzl_animation_unload_begin_values (DzlAnimation *animation) +{ + Tween *tween; + guint i; + + g_assert (DZL_IS_ANIMATION (animation)); + + for (i = 0; i < animation->tweens->len; i++) + { + tween = &g_array_index (animation->tweens, Tween, i); + g_value_reset (&tween->begin); + } +} + + +/** + * dzl_animation_get_offset: + * @animation: A #DzlAnimation. + * @frame_time: the time to present the frame, or 0 for current timing. + * + * Retrieves the position within the animation from 0.0 to 1.0. This + * value is calculated using the msec of the beginning of the animation + * and the current time. + * + * Returns: The offset of the animation from 0.0 to 1.0. + */ +static gdouble +dzl_animation_get_offset (DzlAnimation *animation, + gint64 frame_time) +{ + g_assert (DZL_IS_ANIMATION (animation)); + + if (frame_time == 0) + { + if (animation->frame_clock != NULL) + frame_time = gdk_frame_clock_get_frame_time (animation->frame_clock); + else + frame_time = g_get_monotonic_time (); + } + + frame_time = CLAMP (frame_time, animation->begin_time, animation->end_time); + + /* Check end_time first in case end_time == begin_time */ + if (frame_time == animation->end_time) + return 1.0; + else if (frame_time == animation->begin_time) + return 0.0; + + return (frame_time - animation->begin_time) / (gdouble)(animation->duration_msec * 1000L); +} + + +/** + * dzl_animation_update_property: + * @animation: (in): A #DzlAnimation. + * @target: (in): A #GObject. + * @tween: (in): a #Tween containing the property. + * @value: (in): The new value for the property. + * + * Updates the value of a property on an object using @value. + * + * Side effects: The property of @target is updated. + */ +static void +dzl_animation_update_property (DzlAnimation *animation, + gpointer target, + Tween *tween, + const GValue *value) +{ + g_assert (DZL_IS_ANIMATION (animation)); + g_assert (G_IS_OBJECT (target)); + g_assert (tween); + g_assert (value); + + g_object_set_property (target, tween->pspec->name, value); +} + + +/** + * dzl_animation_update_child_property: + * @animation: (in): A #DzlAnimation. + * @target: (in): A #GObject. + * @tween: (in): A #Tween containing the property. + * @value: (in): The new value for the property. + * + * Updates the value of the parent widget of the target to @value. + * + * Side effects: The property of @target's parent widget is updated. + */ +static void +dzl_animation_update_child_property (DzlAnimation *animation, + gpointer target, + Tween *tween, + const GValue *value) +{ + GtkWidget *parent; + + g_assert (DZL_IS_ANIMATION (animation)); + g_assert (G_IS_OBJECT (target)); + g_assert (tween); + g_assert (value); + + parent = gtk_widget_get_parent (GTK_WIDGET (target)); + gtk_container_child_set_property (GTK_CONTAINER (parent), + target, + tween->pspec->name, + value); +} + + +/** + * dzl_animation_get_value_at_offset: + * @animation: (in): A #DzlAnimation. + * @offset: (in): The offset in the animation from 0.0 to 1.0. + * @tween: (in): A #Tween containing the property. + * @value: (out): A #GValue in which to store the property. + * + * Retrieves a value for a particular position within the animation. + * + * Side effects: None. + */ +static void +dzl_animation_get_value_at_offset (DzlAnimation *animation, + gdouble offset, + Tween *tween, + GValue *value) +{ + g_assert (DZL_IS_ANIMATION (animation)); + g_assert (tween != NULL); + g_assert (value != NULL); + g_assert (value->g_type == tween->pspec->value_type); + + if (value->g_type < LAST_FUNDAMENTAL) + { + /* + * If you hit the following assertion, you need to add a function + * to create the new value at the given offset. + */ + g_assert (tween_funcs[value->g_type]); + tween_funcs[value->g_type](&tween->begin, &tween->end, value, offset); + } + else + { + /* + * TODO: Support complex transitions. + */ + if (offset >= 1.0) + g_value_copy (&tween->end, value); + } +} + +static void +dzl_animation_set_frame_clock (DzlAnimation *animation, + GdkFrameClock *frame_clock) +{ + if (animation->frame_clock != frame_clock) + { + g_clear_object (&animation->frame_clock); + animation->frame_clock = frame_clock ? g_object_ref (frame_clock) : NULL; + } +} + +static void +dzl_animation_set_target (DzlAnimation *animation, + gpointer target) +{ + g_assert (!animation->target); + + animation->target = g_object_ref (target); + + if (GTK_IS_WIDGET (animation->target)) + dzl_animation_set_frame_clock (animation, + gtk_widget_get_frame_clock (animation->target)); +} + + +/** + * dzl_animation_tick: + * @animation: (in): A #DzlAnimation. + * + * Moves the object properties to the next position in the animation. + * + * Returns: %TRUE if the animation has not completed; otherwise %FALSE. + * Side effects: None. + */ +static gboolean +dzl_animation_tick (DzlAnimation *animation, + gdouble offset) +{ + gdouble alpha; + GValue value = { 0 }; + Tween *tween; + guint i; + + g_assert (DZL_IS_ANIMATION (animation)); + + if (offset == animation->last_offset) + return offset < 1.0; + + alpha = alpha_funcs[animation->mode](offset); + + /* + * Update property values. + */ + for (i = 0; i < animation->tweens->len; i++) + { + tween = &g_array_index (animation->tweens, Tween, i); + g_value_init (&value, tween->pspec->value_type); + dzl_animation_get_value_at_offset (animation, alpha, tween, &value); + if (!tween->is_child) + { + dzl_animation_update_property (animation, + animation->target, + tween, + &value); + } + else + { + dzl_animation_update_child_property (animation, + animation->target, + tween, + &value); + } + g_value_unset (&value); + } + + /* + * Notify anyone interested in the tick signal. + */ + g_signal_emit (animation, signals[TICK], 0); + + /* + * Flush any outstanding events to the graphics server (in the case of X). + */ +#if !GTK_CHECK_VERSION (3, 13, 0) + if (GTK_IS_WIDGET (animation->target)) + { + GdkWindow *window; + + if ((window = gtk_widget_get_window (GTK_WIDGET (animation->target)))) + gdk_window_flush (window); + } +#endif + + animation->last_offset = offset; + + return offset < 1.0; +} + + +/** + * dzl_animation_timeout_cb: + * @user_data: (in): A #DzlAnimation. + * + * Timeout from the main loop to move to the next step of the animation. + * + * Returns: %TRUE until the animation has completed; otherwise %FALSE. + * Side effects: None. + */ +static gboolean +dzl_animation_timeout_cb (gpointer user_data) +{ + DzlAnimation *animation = user_data; + gboolean ret; + gdouble offset; + + offset = dzl_animation_get_offset (animation, 0); + + if (!(ret = dzl_animation_tick (animation, offset))) + dzl_animation_stop (animation); + + return ret; +} + + +static gboolean +dzl_animation_widget_tick_cb (GdkFrameClock *frame_clock, + DzlAnimation *animation) +{ + gboolean ret = G_SOURCE_REMOVE; + + g_assert (GDK_IS_FRAME_CLOCK (frame_clock)); + g_assert (DZL_IS_ANIMATION (animation)); + + if (animation->tween_handler) + { + gdouble offset; + + offset = dzl_animation_get_offset (animation, 0); + + if (!(ret = dzl_animation_tick (animation, offset))) + dzl_animation_stop (animation); + } + + return ret; +} + + +static void +dzl_animation_widget_after_paint_cb (GdkFrameClock *frame_clock, + DzlAnimation *animation) +{ + gint64 base_time; + gint64 interval; + gint64 next_frame_time; + gdouble offset; + + g_assert (GDK_IS_FRAME_CLOCK (frame_clock)); + g_assert (DZL_IS_ANIMATION (animation)); + + base_time = gdk_frame_clock_get_frame_time (frame_clock); + gdk_frame_clock_get_refresh_info (frame_clock, base_time, &interval, &next_frame_time); + + offset = dzl_animation_get_offset (animation, next_frame_time); + + dzl_animation_tick (animation, offset); +} + + +/** + * dzl_animation_start: + * @animation: (in): A #DzlAnimation. + * + * Start the animation. When the animation stops, the internal reference will + * be dropped and the animation may be finalized. + * + * Side effects: None. + */ +void +dzl_animation_start (DzlAnimation *animation) +{ + g_return_if_fail (DZL_IS_ANIMATION (animation)); + g_return_if_fail (!animation->tween_handler); + + g_object_ref_sink (animation); + dzl_animation_load_begin_values (animation); + + /* + * We want the real current time instead of the GdkFrameClocks current time + * because if the clock was asleep, it could be innaccurate. + */ + + if (animation->frame_clock) + { + animation->begin_time = gdk_frame_clock_get_frame_time (animation->frame_clock); + animation->end_time = animation->begin_time + (animation->duration_msec * 1000L); + animation->tween_handler = + g_signal_connect_object (animation->frame_clock, + "update", + G_CALLBACK (dzl_animation_widget_tick_cb), + animation, + 0); + animation->after_paint_handler = + g_signal_connect_object (animation->frame_clock, + "after-paint", + G_CALLBACK (dzl_animation_widget_after_paint_cb), + animation, + 0); + gdk_frame_clock_begin_updating (animation->frame_clock); + } + else + { + animation->begin_time = g_get_monotonic_time (); + animation->end_time = animation->begin_time + (animation->duration_msec * 1000L); + animation->tween_handler = dzl_frame_source_add (FALLBACK_FRAME_RATE, + dzl_animation_timeout_cb, + animation); + } +} + + +static void +dzl_animation_notify (DzlAnimation *self) +{ + g_assert (DZL_IS_ANIMATION (self)); + + if (self->notify != NULL) + { + GDestroyNotify notify = self->notify; + gpointer data = self->notify_data; + + self->notify = NULL; + self->notify_data = NULL; + + notify (data); + } +} + + +/** + * dzl_animation_stop: + * @animation: (nullable): A #DzlAnimation. + * + * Stops a running animation. The internal reference to the animation is + * dropped and therefore may cause the object to finalize. + * + * As a convenience, this function accepts %NULL for @animation but + * does nothing if that should occur. + */ +void +dzl_animation_stop (DzlAnimation *animation) +{ + if (animation == NULL) + return; + + if (animation->tween_handler) + { + if (animation->frame_clock) + { + gdk_frame_clock_end_updating (animation->frame_clock); + g_signal_handler_disconnect (animation->frame_clock, animation->tween_handler); + g_signal_handler_disconnect (animation->frame_clock, animation->after_paint_handler); + animation->tween_handler = 0; + } + else + { + g_source_remove (animation->tween_handler); + animation->tween_handler = 0; + } + dzl_animation_unload_begin_values (animation); + dzl_animation_notify (animation); + g_object_unref (animation); + } +} + + +/** + * dzl_animation_add_property: + * @animation: (in): A #DzlAnimation. + * @pspec: (in): A #ParamSpec of @target or a #GtkWidget's parent. + * @value: (in): The new value for the property at the end of the animation. + * + * Adds a new property to the set of properties to be animated during the + * lifetime of the animation. + * + * Side effects: None. + */ +void +dzl_animation_add_property (DzlAnimation *animation, + GParamSpec *pspec, + const GValue *value) +{ + Tween tween = { 0 }; + GType type; + + g_return_if_fail (DZL_IS_ANIMATION (animation)); + g_return_if_fail (pspec != NULL); + g_return_if_fail (value != NULL); + g_return_if_fail (value->g_type); + g_return_if_fail (animation->target); + g_return_if_fail (!animation->tween_handler); + + type = G_TYPE_FROM_INSTANCE (animation->target); + tween.is_child = !g_type_is_a (type, pspec->owner_type); + if (tween.is_child) + { + if (!GTK_IS_WIDGET (animation->target)) + { + g_critical (_("Cannot locate property %s in class %s"), + pspec->name, g_type_name (type)); + return; + } + } + + tween.pspec = g_param_spec_ref (pspec); + g_value_init (&tween.begin, pspec->value_type); + g_value_init (&tween.end, pspec->value_type); + g_value_copy (value, &tween.end); + g_array_append_val (animation->tweens, tween); +} + + +/** + * dzl_animation_dispose: + * @object: (in): A #DzlAnimation. + * + * Releases any object references the animation contains. + * + * Side effects: None. + */ +static void +dzl_animation_dispose (GObject *object) +{ + DzlAnimation *self = DZL_ANIMATION (object); + + g_clear_object (&self->target); + g_clear_object (&self->frame_clock); + + G_OBJECT_CLASS (dzl_animation_parent_class)->dispose (object); +} + + +/** + * dzl_animation_finalize: + * @object: (in): A #DzlAnimation. + * + * Finalizes the object and releases any resources allocated. + * + * Side effects: None. + */ +static void +dzl_animation_finalize (GObject *object) +{ + DzlAnimation *self = DZL_ANIMATION (object); + Tween *tween; + guint i; + + for (i = 0; i < self->tweens->len; i++) + { + tween = &g_array_index (self->tweens, Tween, i); + g_value_unset (&tween->begin); + g_value_unset (&tween->end); + g_param_spec_unref (tween->pspec); + } + + g_array_unref (self->tweens); + + G_OBJECT_CLASS (dzl_animation_parent_class)->finalize (object); +} + + +/** + * dzl_animation_set_property: + * @object: (in): A #GObject. + * @prop_id: (in): The property identifier. + * @value: (in): The given property. + * @pspec: (in): A #ParamSpec. + * + * Set a given #GObject property. + */ +static void +dzl_animation_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlAnimation *animation = DZL_ANIMATION (object); + + switch (prop_id) + { + case PROP_DURATION: + animation->duration_msec = g_value_get_uint (value) * slow_down_factor; + break; + + case PROP_FRAME_CLOCK: + dzl_animation_set_frame_clock (animation, g_value_get_object (value)); + break; + + case PROP_MODE: + animation->mode = g_value_get_enum (value); + break; + + case PROP_TARGET: + dzl_animation_set_target (animation, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + + +/** + * dzl_animation_class_init: + * @klass: (in): A #DzlAnimationClass. + * + * Initializes the GObjectClass. + * + * Side effects: Properties, signals, and vtables are initialized. + */ +static void +dzl_animation_class_init (DzlAnimationClass *klass) +{ + GObjectClass *object_class; + const gchar *slow_down_factor_env; + + debug = !!g_getenv ("DZL_ANIMATION_DEBUG"); + slow_down_factor_env = g_getenv ("DZL_ANIMATION_SLOW_DOWN_FACTOR"); + + if (slow_down_factor_env) + slow_down_factor = MAX (1, atoi (slow_down_factor_env)); + + object_class = G_OBJECT_CLASS (klass); + object_class->dispose = dzl_animation_dispose; + object_class->finalize = dzl_animation_finalize; + object_class->set_property = dzl_animation_set_property; + + /** + * DzlAnimation:duration: + * + * The "duration" property is the total number of milliseconds that the + * animation should run before being completed. + */ + properties[PROP_DURATION] = + g_param_spec_uint ("duration", + "Duration", + "The duration of the animation", + 0, + G_MAXUINT, + 250, + (G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + properties[PROP_FRAME_CLOCK] = + g_param_spec_object ("frame-clock", + "Frame Clock", + "An optional frame-clock to synchronize with.", + GDK_TYPE_FRAME_CLOCK, + (G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * DzlAnimation:mode: + * + * The "mode" property is the Alpha function that should be used to + * determine the offset within the animation based on the current + * offset in the animations duration. + */ + properties[PROP_MODE] = + g_param_spec_enum ("mode", + "Mode", + "The animation mode", + DZL_TYPE_ANIMATION_MODE, + DZL_ANIMATION_LINEAR, + (G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * DzlAnimation:target: + * + * The "target" property is the #GObject that should have its properties + * animated. + */ + properties[PROP_TARGET] = + g_param_spec_object ("target", + "Target", + "The target of the animation", + G_TYPE_OBJECT, + (G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + /** + * DzlAnimation::tick: + * + * The "tick" signal is emitted on each frame in the animation. + */ + signals[TICK] = g_signal_new ("tick", + DZL_TYPE_ANIMATION, + G_SIGNAL_RUN_FIRST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + +#define SET_ALPHA(_T, _t) \ + alpha_funcs[DZL_ANIMATION_ ## _T] = dzl_animation_alpha_ ## _t + + SET_ALPHA (LINEAR, linear); + SET_ALPHA (EASE_IN_QUAD, ease_in_quad); + SET_ALPHA (EASE_OUT_QUAD, ease_out_quad); + SET_ALPHA (EASE_IN_OUT_QUAD, ease_in_out_quad); + SET_ALPHA (EASE_IN_CUBIC, ease_in_cubic); + SET_ALPHA (EASE_OUT_CUBIC, ease_out_cubic); + SET_ALPHA (EASE_IN_OUT_CUBIC, ease_in_out_cubic); + +#define SET_TWEEN(_T, _t) \ + G_STMT_START { \ + guint idx = G_TYPE_ ## _T; \ + tween_funcs[idx] = tween_ ## _t; \ + } G_STMT_END + + SET_TWEEN (INT, int); + SET_TWEEN (UINT, uint); + SET_TWEEN (LONG, long); + SET_TWEEN (ULONG, ulong); + SET_TWEEN (FLOAT, float); + SET_TWEEN (DOUBLE, double); +} + + +/** + * dzl_animation_init: + * @animation: (in): A #DzlAnimation. + * + * Initializes the #DzlAnimation instance. + * + * Side effects: Everything. + */ +static void +dzl_animation_init (DzlAnimation *animation) +{ + animation->duration_msec = 250; + animation->mode = DZL_ANIMATION_EASE_IN_OUT_QUAD; + animation->tweens = g_array_new (FALSE, FALSE, sizeof (Tween)); + animation->last_offset = -G_MINDOUBLE; +} + + +/** + * dzl_animation_mode_get_type: + * + * Retrieves the GType for #DzlAnimationMode. + * + * Returns: A GType. + * Side effects: GType registered on first call. + */ +GType +dzl_animation_mode_get_type (void) +{ + static GType type_id = 0; + static const GEnumValue values[] = { + { DZL_ANIMATION_LINEAR, "DZL_ANIMATION_LINEAR", "linear" }, + { DZL_ANIMATION_EASE_IN_QUAD, "DZL_ANIMATION_EASE_IN_QUAD", "ease-in-quad" }, + { DZL_ANIMATION_EASE_IN_OUT_QUAD, "DZL_ANIMATION_EASE_IN_OUT_QUAD", "ease-in-out-quad" }, + { DZL_ANIMATION_EASE_OUT_QUAD, "DZL_ANIMATION_EASE_OUT_QUAD", "ease-out-quad" }, + { DZL_ANIMATION_EASE_IN_CUBIC, "DZL_ANIMATION_EASE_IN_CUBIC", "ease-in-cubic" }, + { DZL_ANIMATION_EASE_OUT_CUBIC, "DZL_ANIMATION_EASE_OUT_CUBIC", "ease-out-cubic" }, + { DZL_ANIMATION_EASE_IN_OUT_CUBIC, "DZL_ANIMATION_EASE_IN_OUT_CUBIC", "ease-in-out-cubic" }, + { 0 } + }; + + if (G_UNLIKELY (!type_id)) + type_id = g_enum_register_static ("DzlAnimationMode", values); + return type_id; +} + +/** + * dzl_object_animatev: + * @object: A #GObject. + * @mode: The animation mode. + * @duration_msec: The duration in milliseconds. + * @frame_clock: (nullable): The #GdkFrameClock to synchronize to. + * @first_property: The first property to animate. + * @args: A variadac list of arguments + * + * Returns: (transfer none): A #DzlAnimation. + */ +DzlAnimation * +dzl_object_animatev (gpointer object, + DzlAnimationMode mode, + guint duration_msec, + GdkFrameClock *frame_clock, + const gchar *first_property, + va_list args) +{ + DzlAnimation *animation; + GObjectClass *klass; + GObjectClass *pklass; + const gchar *name; + GParamSpec *pspec; + GtkWidget *parent; + GValue value = { 0 }; + gchar *error = NULL; + GType type; + GType ptype; + gboolean enable_animations; + + g_return_val_if_fail (first_property != NULL, NULL); + g_return_val_if_fail (mode < DZL_ANIMATION_LAST, NULL); + + if ((frame_clock == NULL) && GTK_IS_WIDGET (object)) + frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (object)); + + /* + * If we have a frame clock, then we must be in the gtk thread and we + * should check GtkSettings for disabled animations. If we are disabled, + * we will just make the timeout immediate. + */ + if (frame_clock != NULL) + { + g_object_get (gtk_settings_get_default (), + "gtk-enable-animations", &enable_animations, + NULL); + + if (enable_animations == FALSE) + duration_msec = 0; + } + + name = first_property; + type = G_TYPE_FROM_INSTANCE (object); + klass = G_OBJECT_GET_CLASS (object); + animation = g_object_new (DZL_TYPE_ANIMATION, + "duration", duration_msec, + "frame-clock", frame_clock, + "mode", mode, + "target", object, + NULL); + + do + { + /* + * First check for the property on the object. If that does not exist + * then check if the object has a parent and look at its child + * properties (if it's a GtkWidget). + */ + if (!(pspec = g_object_class_find_property (klass, name))) + { + if (!g_type_is_a (type, GTK_TYPE_WIDGET)) + { + g_critical (_("Failed to find property %s in %s"), + name, g_type_name (type)); + goto failure; + } + if (!(parent = gtk_widget_get_parent (object))) + { + g_critical (_("Failed to find property %s in %s"), + name, g_type_name (type)); + goto failure; + } + pklass = G_OBJECT_GET_CLASS (parent); + ptype = G_TYPE_FROM_INSTANCE (parent); + if (!(pspec = gtk_container_class_find_child_property (pklass, name))) + { + g_critical (_("Failed to find property %s in %s or parent %s"), + name, g_type_name (type), g_type_name (ptype)); + goto failure; + } + } + + g_value_init (&value, pspec->value_type); + G_VALUE_COLLECT (&value, args, 0, &error); + if (error != NULL) + { + g_critical (_("Failed to retrieve va_list value: %s"), error); + g_free (error); + goto failure; + } + + dzl_animation_add_property (animation, pspec, &value); + g_value_unset (&value); + } + while ((name = va_arg (args, const gchar *))); + + dzl_animation_start (animation); + + return animation; + +failure: + g_object_ref_sink (animation); + g_object_unref (animation); + return NULL; +} + +/** + * dzl_object_animate: + * @object: (in): A #GObject. + * @mode: (in): The animation mode. + * @duration_msec: (in): The duration in milliseconds. + * @first_property: (in): The first property to animate. + * + * Animates the properties of @object. The can be set in a similar manner to g_object_set(). They + * will be animated from their current value to the target value over the time period. + * + * Return value: (transfer none): A #DzlAnimation. + * Side effects: None. + */ +DzlAnimation* +dzl_object_animate (gpointer object, + DzlAnimationMode mode, + guint duration_msec, + GdkFrameClock *frame_clock, + const gchar *first_property, + ...) +{ + DzlAnimation *animation; + va_list args; + + va_start (args, first_property); + animation = dzl_object_animatev (object, + mode, + duration_msec, + frame_clock, + first_property, + args); + va_end (args); + + return animation; +} + +/** + * dzl_object_animate_full: + * + * Return value: (transfer none): A #DzlAnimation. + */ +DzlAnimation* +dzl_object_animate_full (gpointer object, + DzlAnimationMode mode, + guint duration_msec, + GdkFrameClock *frame_clock, + GDestroyNotify notify, + gpointer notify_data, + const gchar *first_property, + ...) +{ + DzlAnimation *animation; + va_list args; + + va_start (args, first_property); + animation = dzl_object_animatev (object, + mode, + duration_msec, + frame_clock, + first_property, + args); + va_end (args); + + animation->notify = notify; + animation->notify_data = notify_data; + + return animation; +} + +guint +dzl_animation_calculate_duration (GdkMonitor *monitor, + gdouble from_value, + gdouble to_value) +{ + GdkRectangle geom; + gdouble distance_units; + gdouble distance_mm; + gdouble mm_per_frame; + gint height_mm; + gint refresh_rate; + gint n_frames; + guint ret; + +#define MM_PER_SECOND (150.0) +#define MIN_FRAMES_PER_ANIM (5) +#define MAX_FRAMES_PER_ANIM (500) + + g_assert (GDK_IS_MONITOR (monitor)); + g_assert (from_value >= 0.0); + g_assert (to_value >= 0.0); + + /* + * Get various monitor information we'll need to calculate the duration of + * the animation. We need the physical space of the monitor, the refresh + * rate, and geometry so that we can limit how many device units we will + * traverse per-frame of the animation. Failure to deal with the physical + * space results in jittery animations to the user. + * + * It would also be nice to take into account the acceleration curve so that + * we know the max amount of jump per frame, but that is getting into + * diminishing returns since we can just average it out. + */ + height_mm = gdk_monitor_get_height_mm (monitor); + gdk_monitor_get_geometry (monitor, &geom); + refresh_rate = gdk_monitor_get_refresh_rate (monitor); + if (refresh_rate == 0) + refresh_rate = 60000; + + /* + * The goal here is to determine the number of millimeters that we need to + * animate given a transition of distance_unit pixels. Since we are dealing + * with physical units (mm), we don't need to take into account the device + * scale underneath the widget. The equation comes out the same. + */ + + distance_units = ABS (from_value - to_value); + distance_mm = distance_units / (gdouble)geom.height * height_mm; + mm_per_frame = MM_PER_SECOND / (refresh_rate / 1000.0); + n_frames = (distance_mm / mm_per_frame) + 1; + + ret = n_frames * (1000.0 / (refresh_rate / 1000.0)); + ret = CLAMP (ret, + MIN_FRAMES_PER_ANIM * (1000000.0 / refresh_rate), + MAX_FRAMES_PER_ANIM * (1000000.0 / refresh_rate)); + + return ret; + +#undef MM_PER_SECOND +#undef MIN_FRAMES_PER_ANIM +#undef MAX_FRAMES_PER_ANIM +} diff --git a/src/animation/dzl-animation.h b/src/animation/dzl-animation.h new file mode 100644 index 0000000..ce6e532 --- /dev/null +++ b/src/animation/dzl-animation.h @@ -0,0 +1,89 @@ +/* dzl-animation.h + * + * Copyright (C) 2010-2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#ifndef DZL_ANIMATION_H +#define DZL_ANIMATION_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_ANIMATION (dzl_animation_get_type()) +#define DZL_TYPE_ANIMATION_MODE (dzl_animation_mode_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlAnimation, dzl_animation, DZL, ANIMATION, GInitiallyUnowned) + +enum _DzlAnimationMode +{ + DZL_ANIMATION_LINEAR, + DZL_ANIMATION_EASE_IN_QUAD, + DZL_ANIMATION_EASE_OUT_QUAD, + DZL_ANIMATION_EASE_IN_OUT_QUAD, + DZL_ANIMATION_EASE_IN_CUBIC, + DZL_ANIMATION_EASE_OUT_CUBIC, + DZL_ANIMATION_EASE_IN_OUT_CUBIC, + + DZL_ANIMATION_LAST +}; + +typedef enum _DzlAnimationMode DzlAnimationMode; + +DZL_AVAILABLE_IN_ALL +GType dzl_animation_mode_get_type (void); +DZL_AVAILABLE_IN_ALL +void dzl_animation_start (DzlAnimation *animation); +DZL_AVAILABLE_IN_ALL +void dzl_animation_stop (DzlAnimation *animation); +DZL_AVAILABLE_IN_ALL +void dzl_animation_add_property (DzlAnimation *animation, + GParamSpec *pspec, + const GValue *value); +DZL_AVAILABLE_IN_ALL +DzlAnimation *dzl_object_animatev (gpointer object, + DzlAnimationMode mode, + guint duration_msec, + GdkFrameClock *frame_clock, + const gchar *first_property, + va_list args); +DZL_AVAILABLE_IN_ALL +DzlAnimation* dzl_object_animate (gpointer object, + DzlAnimationMode mode, + guint duration_msec, + GdkFrameClock *frame_clock, + const gchar *first_property, + ...) G_GNUC_NULL_TERMINATED; +DZL_AVAILABLE_IN_ALL +DzlAnimation* dzl_object_animate_full (gpointer object, + DzlAnimationMode mode, + guint duration_msec, + GdkFrameClock *frame_clock, + GDestroyNotify notify, + gpointer notify_data, + const gchar *first_property, + ...) G_GNUC_NULL_TERMINATED; +DZL_AVAILABLE_IN_ALL +guint dzl_animation_calculate_duration (GdkMonitor *monitor, + gdouble from_value, + gdouble to_value); + +G_END_DECLS + +#endif /* DZL_ANIMATION_H */ diff --git a/src/animation/dzl-box-theatric.c b/src/animation/dzl-box-theatric.c new file mode 100644 index 0000000..618c001 --- /dev/null +++ b/src/animation/dzl-box-theatric.c @@ -0,0 +1,445 @@ +/* dzl-box-theatric.c + * + * Copyright (C) 2014 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-box-theatric" + +#include "config.h" + +#include + +#include "animation/dzl-box-theatric.h" +#include "util/dzl-cairo.h" + +struct _DzlBoxTheatric +{ + GObject parent_instance; + + GtkWidget *target; + GtkWidget *toplevel; + + GIcon *icon; + cairo_surface_t *icon_surface; + guint icon_surface_size; + + GdkRectangle area; + GdkRectangle last_area; + GdkRGBA background_rgba; + gdouble alpha; + + guint draw_handler; + + guint background_set : 1; + guint pixbuf_failed : 1; +}; + +enum { + PROP_0, + PROP_ALPHA, + PROP_BACKGROUND, + PROP_HEIGHT, + PROP_ICON, + PROP_TARGET, + PROP_WIDTH, + PROP_X, + PROP_Y, + PROP_SURFACE, + LAST_PROP +}; + +G_DEFINE_TYPE (DzlBoxTheatric, dzl_box_theatric, G_TYPE_OBJECT) + +static GParamSpec *properties[LAST_PROP]; + +static void +get_toplevel_rect (DzlBoxTheatric *theatric, + GdkRectangle *area) +{ + gtk_widget_translate_coordinates (theatric->target, theatric->toplevel, + theatric->area.x, theatric->area.y, + &area->x, &area->y); + + area->width = theatric->area.width; + area->height = theatric->area.height; +} + +static gboolean +on_toplevel_draw (GtkWidget *widget, + cairo_t *cr, + DzlBoxTheatric *self) +{ + GdkRectangle area; + + g_assert (DZL_IS_BOX_THEATRIC (self)); + + get_toplevel_rect (self, &area); + +#if 0 + g_print ("Drawing on %s at %d,%d %dx%d\n", + g_type_name (G_TYPE_FROM_INSTANCE (widget)), + area.x, area.y, area.width, area.height); +#endif + + if (self->background_set) + { + GdkRGBA bg; + + bg = self->background_rgba; + bg.alpha = self->alpha; + + dzl_cairo_rounded_rectangle (cr, &area, 3, 3); + gdk_cairo_set_source_rgba (cr, &bg); + cairo_fill (cr); + } + + /* Load an icon if necessary and cache the surface */ + if (self->icon != NULL && self->icon_surface == NULL && !self->pixbuf_failed) + { + g_autoptr(GtkIconInfo) icon_info = NULL; + GtkIconTheme *icon_theme; + gint width; + + width = area.width * 4; + icon_theme = gtk_icon_theme_get_default (); + icon_info = gtk_icon_theme_lookup_by_gicon (icon_theme, + self->icon, + width, + GTK_ICON_LOOKUP_FORCE_SIZE); + + if (icon_info != NULL) + { + GdkWindow *window = gtk_widget_get_window (widget); + g_autoptr(GdkPixbuf) pixbuf = NULL; + GtkStyleContext *context; + + context = gtk_widget_get_style_context (self->target); + pixbuf = gtk_icon_info_load_symbolic_for_context (icon_info, context, NULL, NULL); + + if (pixbuf != NULL) + { + self->icon_surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, 0, window); + self->icon_surface_size = width; + self->pixbuf_failed = FALSE; + } + else + { + self->pixbuf_failed = TRUE; + } + } + } + + if (self->icon_surface != NULL) + { + cairo_translate (cr, area.x, area.y); + cairo_rectangle (cr, 0, 0, area.width, area.height); + if (self->icon_surface_size != 0) + cairo_scale (cr, + area.width / (gdouble)self->icon_surface_size, + area.height / (gdouble)self->icon_surface_size); + cairo_set_source_surface (cr, self->icon_surface, 0, 0); + cairo_paint_with_alpha (cr, self->alpha); + } + + self->last_area = area; + + /* The invalidation rectangle does not appear to be inclusive. + * This means that occcasionly we seem to have a stragler line. + * But setting this to one seems to make that go away for the + * current tests I have. + */ + self->last_area.width += 1; + self->last_area.height += 1; + + return FALSE; +} + +static void +dzl_box_theatric_notify (GObject *object, + GParamSpec *pspec) +{ + DzlBoxTheatric *self = DZL_BOX_THEATRIC (object); + + if (G_OBJECT_CLASS (dzl_box_theatric_parent_class)->notify) + G_OBJECT_CLASS (dzl_box_theatric_parent_class)->notify (object, pspec); + + if (self->target && self->toplevel) + { + GdkWindow *window; + GdkRectangle area; + + get_toplevel_rect (DZL_BOX_THEATRIC (object), &area); + +#if 0 + g_print (" Invalidate : %d,%d %dx%d\n", + area.x, area.y, area.width, area.height); +#endif + + window = gtk_widget_get_window (self->toplevel); + + if (window != NULL) + { + gdk_window_invalidate_rect (window, &self->last_area, TRUE); + gdk_window_invalidate_rect (window, &area, TRUE); + } + } +} + +static void +dzl_box_theatric_dispose (GObject *object) +{ + DzlBoxTheatric *self = DZL_BOX_THEATRIC (object); + + if (self->target) + { + if (self->draw_handler && self->toplevel) + { + g_signal_handler_disconnect (self->toplevel, self->draw_handler); + self->draw_handler = 0; + } + g_object_remove_weak_pointer (G_OBJECT (self->target), + (gpointer *) &self->target); + self->target = NULL; + } + + g_clear_pointer (&self->icon_surface, cairo_surface_destroy); + g_clear_object (&self->icon); + + G_OBJECT_CLASS (dzl_box_theatric_parent_class)->dispose (object); +} + +static void +dzl_box_theatric_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlBoxTheatric *theatric = DZL_BOX_THEATRIC (object); + + switch (prop_id) + { + case PROP_ALPHA: + g_value_set_double (value, theatric->alpha); + break; + + case PROP_BACKGROUND: + g_value_take_string (value, + gdk_rgba_to_string (&theatric->background_rgba)); + break; + + case PROP_HEIGHT: + g_value_set_int (value, theatric->area.height); + break; + + case PROP_ICON: + g_value_set_object (value, theatric->icon); + break; + + case PROP_TARGET: + g_value_set_object (value, theatric->target); + break; + + case PROP_WIDTH: + g_value_set_int (value, theatric->area.width); + break; + + case PROP_X: + g_value_set_int (value, theatric->area.x); + break; + + case PROP_Y: + g_value_set_int (value, theatric->area.y); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_box_theatric_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlBoxTheatric *theatric = DZL_BOX_THEATRIC (object); + + switch (prop_id) + { + case PROP_ALPHA: + theatric->alpha = g_value_get_double (value); + break; + + case PROP_BACKGROUND: + { + const gchar *str = g_value_get_string (value); + + if (str == NULL) + { + gdk_rgba_parse (&theatric->background_rgba, "#000000"); + theatric->background_rgba.alpha = 0; + theatric->background_set = FALSE; + } + else + { + gdk_rgba_parse (&theatric->background_rgba, str); + theatric->background_set = TRUE; + } + } + break; + + case PROP_HEIGHT: + theatric->area.height = g_value_get_int (value); + break; + + case PROP_ICON: + g_clear_pointer (&theatric->icon_surface, cairo_surface_destroy); + g_clear_object (&theatric->icon); + theatric->icon = g_value_dup_object (value); + theatric->pixbuf_failed = FALSE; + break; + + case PROP_SURFACE: + g_clear_pointer (&theatric->icon_surface, cairo_surface_destroy); + theatric->icon_surface = g_value_get_pointer (value); + if (theatric->icon_surface != NULL) + cairo_surface_reference (theatric->icon_surface); + break; + + case PROP_TARGET: + theatric->target = g_value_get_object (value); + theatric->toplevel = gtk_widget_get_toplevel (theatric->target); + g_object_add_weak_pointer (G_OBJECT (theatric->target), + (gpointer *) &theatric->target); + theatric->draw_handler = + g_signal_connect_after (theatric->toplevel, + "draw", + G_CALLBACK (on_toplevel_draw), + theatric); + break; + + case PROP_WIDTH: + theatric->area.width = g_value_get_int (value); + break; + + case PROP_X: + theatric->area.x = g_value_get_int (value); + break; + + case PROP_Y: + theatric->area.y = g_value_get_int (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } + + g_object_notify_by_pspec (object, pspec); +} + +static void +dzl_box_theatric_class_init (DzlBoxTheatricClass *klass) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->dispose = dzl_box_theatric_dispose; + object_class->notify = dzl_box_theatric_notify; + object_class->get_property = dzl_box_theatric_get_property; + object_class->set_property = dzl_box_theatric_set_property; + + properties[PROP_ALPHA] = + g_param_spec_double ("alpha", + "Alpha", + "Alpha", + 0.0, + 1.0, + 1.0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties[PROP_BACKGROUND] = + g_param_spec_string ("background", + "background", + "background", + "#000000", + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties[PROP_HEIGHT] = + g_param_spec_int ("height", + "height", + "height", + 0, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_ICON] = + g_param_spec_object ("icon", + "Icon", + "The GIcon to render over the background", + G_TYPE_ICON, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties[PROP_TARGET] = + g_param_spec_object ("target", + "Target", + "Target", + GTK_TYPE_WIDGET, + (G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + properties[PROP_WIDTH] = + g_param_spec_int ("width", + "width", + "width", + 0, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties[PROP_X] = + g_param_spec_int ("x", + "x", + "x", + G_MININT, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties[PROP_Y] = + g_param_spec_int ("y", + "y", + "y", + G_MININT, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties[PROP_SURFACE] = + g_param_spec_pointer ("surface", + "Surface", + "Surface", + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_box_theatric_init (DzlBoxTheatric *theatric) +{ + theatric->alpha = 1.0; +} diff --git a/src/animation/dzl-box-theatric.h b/src/animation/dzl-box-theatric.h new file mode 100644 index 0000000..7413adc --- /dev/null +++ b/src/animation/dzl-box-theatric.h @@ -0,0 +1,35 @@ +/* dzl-box-theatric.h + * + * Copyright (C) 2014 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_BOX_THEATRIC_H +#define DZL_BOX_THEATRIC_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_BOX_THEATRIC (dzl_box_theatric_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlBoxTheatric, dzl_box_theatric, DZL, BOX_THEATRIC, GObject) + +G_END_DECLS + +#endif /* DZL_BOX_THEATRIC_H */ diff --git a/src/animation/dzl-frame-source.c b/src/animation/dzl-frame-source.c new file mode 100644 index 0000000..7f4a87e --- /dev/null +++ b/src/animation/dzl-frame-source.c @@ -0,0 +1,134 @@ +/* dzl-frame-source.c + * + * Copyright (C) 2010-2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-frame-source" + +#include "config.h" + +#include "dzl-frame-source.h" + +typedef struct +{ + GSource parent; + guint fps; + guint frame_count; + gint64 start_time; +} DzlFrameSource; + +static gboolean +dzl_frame_source_prepare (GSource *source, + gint *timeout_) +{ + DzlFrameSource *fsource = (DzlFrameSource *)(gpointer)source; + gint64 current_time; + guint elapsed_time; + guint new_frame_num; + guint frame_time; + + current_time = g_source_get_time(source) / 1000; + elapsed_time = current_time - fsource->start_time; + new_frame_num = elapsed_time * fsource->fps / 1000; + + /* If time has gone backwards or the time since the last frame is + * greater than the two frames worth then reset the time and do a + * frame now */ + if (new_frame_num < fsource->frame_count || + new_frame_num - fsource->frame_count > 2) { + /* Get the frame time rounded up to the nearest ms */ + frame_time = (1000 + fsource->fps - 1) / fsource->fps; + + /* Reset the start time */ + fsource->start_time = current_time; + + /* Move the start time as if one whole frame has elapsed */ + fsource->start_time -= frame_time; + fsource->frame_count = 0; + *timeout_ = 0; + return TRUE; + } else if (new_frame_num > fsource->frame_count) { + *timeout_ = 0; + return TRUE; + } else { + *timeout_ = (fsource->frame_count + 1) * 1000 / fsource->fps - elapsed_time; + return FALSE; + } +} + +static gboolean +dzl_frame_source_check (GSource *source) +{ + gint timeout_; + return dzl_frame_source_prepare(source, &timeout_); +} + +static gboolean +dzl_frame_source_dispatch (GSource *source, + GSourceFunc source_func, + gpointer user_data) +{ + DzlFrameSource *fsource = (DzlFrameSource *)(gpointer)source; + gboolean ret; + + if ((ret = source_func(user_data))) + fsource->frame_count++; + return ret; +} + +static GSourceFuncs source_funcs = { + dzl_frame_source_prepare, + dzl_frame_source_check, + dzl_frame_source_dispatch, +}; + +/** + * dzl_frame_source_add: + * @frames_per_sec: (in): Target frames per second. + * @callback: (in) (scope notified): A #GSourceFunc to execute. + * @user_data: (in): User data for @callback. + * + * Creates a new frame source that will execute when the timeout interval + * for the source has elapsed. The timing will try to synchronize based + * on the end time of the animation. + * + * Returns: A source id that can be removed with g_source_remove(). + */ +guint +dzl_frame_source_add (guint frames_per_sec, + GSourceFunc callback, + gpointer user_data) +{ + DzlFrameSource *fsource; + GSource *source; + guint ret; + + g_return_val_if_fail (frames_per_sec > 0, 0); + g_return_val_if_fail (frames_per_sec <= 120, 0); + + source = g_source_new(&source_funcs, sizeof(DzlFrameSource)); + fsource = (DzlFrameSource *)(gpointer)source; + fsource->fps = frames_per_sec; + fsource->frame_count = 0; + fsource->start_time = g_get_monotonic_time() / 1000; + g_source_set_callback(source, callback, user_data, NULL); + g_source_set_name(source, "DzlFrameSource"); + + ret = g_source_attach(source, NULL); + g_source_unref(source); + + return ret; +} diff --git a/src/animation/dzl-frame-source.h b/src/animation/dzl-frame-source.h new file mode 100644 index 0000000..7dbc4e8 --- /dev/null +++ b/src/animation/dzl-frame-source.h @@ -0,0 +1,32 @@ +/* dzl-frame-source.h + * + * Copyright (C) 2010-2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#ifndef DZL_FRAME_SOURCE_H +#define DZL_FRAME_SOURCE_H + +#include + +G_BEGIN_DECLS + +guint dzl_frame_source_add (guint frames_per_sec, + GSourceFunc callback, + gpointer user_data); + +G_END_DECLS + +#endif /* DZL_FRAME_SOURCE_H */ diff --git a/src/animation/meson.build b/src/animation/meson.build new file mode 100644 index 0000000..48f045f --- /dev/null +++ b/src/animation/meson.build @@ -0,0 +1,15 @@ +animation_headers = [ + 'dzl-animation.h', + 'dzl-box-theatric.h', +] + +animation_sources = [ + 'dzl-animation.c', + 'dzl-box-theatric.c', +] + +libdazzle_public_headers += files(animation_headers) +libdazzle_public_sources += files(animation_sources) +libdazzle_private_sources += files('dzl-frame-source.c') + +install_headers(animation_headers, subdir: join_paths(libdazzle_header_subdir, 'animation')) diff --git a/src/app/dzl-application-window.c b/src/app/dzl-application-window.c new file mode 100644 index 0000000..1c09705 --- /dev/null +++ b/src/app/dzl-application-window.c @@ -0,0 +1,664 @@ +/* dzl-application-window.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-application-window" + +#include "config.h" + +#include "app/dzl-application-window.h" +#include "shortcuts/dzl-shortcut-manager.h" +#include "util/dzl-gtk.h" + +#define DEFAULT_DISMISSAL_SECONDS 3 +#define SHOW_HEADER_WITHIN_DISTANCE 5 + +/** + * SECTION:dzl-application-window + * @title: DzlApplicationWindow + * @short_description: An base application window for applications + * + * The #DzlApplicationWindow class provides a #GtkApplicationWindow subclass + * that integrates well with #DzlApplication. It provides features such as: + * + * - Integration with the #DzlShortcutManager for capture/bubble keyboard + * input events. + * - Native support for fullscreen state by re-parenting the #GtkHeaderBar as + * necessary. #DzlApplicationWindow does expect you to use GtkHeaderBar. + * + * Since: 3.26 + */ + +typedef struct +{ + GtkStack *titlebar_container; + GtkRevealer *titlebar_revealer; + GtkEventBox *event_box; + GtkOverlay *overlay; + + gulong motion_notify_handler; + + guint fullscreen_source; + guint fullscreen_reveal_source; + guint fullscreen : 1; + guint in_key_press : 1; +} DzlApplicationWindowPrivate; + +enum { + PROP_0, + PROP_FULLSCREEN, + N_PROPS +}; + +static void buildable_iface_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (DzlApplicationWindow, dzl_application_window, GTK_TYPE_APPLICATION_WINDOW, + G_ADD_PRIVATE (DzlApplicationWindow) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init)) + +static GParamSpec *properties [N_PROPS]; +static GtkBuildableIface *parent_buildable; + +static gboolean +dzl_application_window_dismissal (DzlApplicationWindow *self) +{ + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + + g_assert (DZL_IS_APPLICATION_WINDOW (self)); + + priv->fullscreen_reveal_source = 0; + + if (dzl_application_window_get_fullscreen (self)) + gtk_revealer_set_reveal_child (priv->titlebar_revealer, FALSE); + + return G_SOURCE_REMOVE; +} + +static void +dzl_application_window_queue_dismissal (DzlApplicationWindow *self) +{ + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + + g_assert (DZL_IS_APPLICATION_WINDOW (self)); + + if (priv->fullscreen_reveal_source != 0) + g_source_remove (priv->fullscreen_reveal_source); + + priv->fullscreen_reveal_source = + gdk_threads_add_timeout_seconds_full (G_PRIORITY_LOW, + DEFAULT_DISMISSAL_SECONDS, + (GSourceFunc) dzl_application_window_dismissal, + self, NULL); +} + +static gboolean +dzl_application_window_real_get_fullscreen (DzlApplicationWindow *self) +{ + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + + g_assert (DZL_IS_APPLICATION_WINDOW (self)); + + return priv->fullscreen; +} + +static void +revealer_set_reveal_child_now (GtkRevealer *revealer, + gboolean reveal_child) +{ + + g_assert (GTK_IS_REVEALER (revealer)); + + gtk_revealer_set_transition_type (revealer, GTK_REVEALER_TRANSITION_TYPE_NONE); + gtk_revealer_set_reveal_child (revealer, reveal_child); + gtk_revealer_set_transition_type (revealer, GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); +} + +static gboolean +dzl_application_window_event_box_motion (DzlApplicationWindow *self, + GdkEventMotion *motion, + GtkEventBox *event_box) +{ + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + GtkWidget *widget = NULL; + GtkWidget *focus; + gint x = 0; + gint y = 0; + + g_assert (DZL_IS_APPLICATION_WINDOW (self)); + g_assert (motion != NULL); + g_assert (GTK_IS_EVENT_BOX (event_box)); + + /* + * If we are focused in the revealer, ignore this. We will have already + * killed the GSource in set_focus(). + */ + focus = gtk_window_get_focus (GTK_WINDOW (self)); + if (focus != NULL && + dzl_gtk_widget_is_ancestor_or_relative (focus, GTK_WIDGET (priv->titlebar_revealer))) + return GDK_EVENT_PROPAGATE; + + /* The widget is stored in GdkWindow user_data */ + gdk_window_get_user_data (motion->window, (gpointer *)&widget); + if (widget == NULL) + return GDK_EVENT_PROPAGATE; + + /* If the headerbar is underneath the pointer or we are within a + * small distance of the edge of the window (and monitor), ensure + * that the titlebar is displayed and queue our next dismissal. + */ + if (dzl_gtk_widget_is_ancestor_or_relative (widget, GTK_WIDGET (priv->titlebar_revealer)) || + gtk_widget_translate_coordinates (widget, GTK_WIDGET (self), motion->x, motion->y, &x, &y)) + { + if (y < SHOW_HEADER_WITHIN_DISTANCE) + { + gtk_revealer_set_reveal_child (priv->titlebar_revealer, TRUE); + dzl_application_window_queue_dismissal (self); + } + } + + return GDK_EVENT_PROPAGATE; +} + +static gboolean +dzl_application_window_complete_fullscreen (DzlApplicationWindow *self) +{ + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + GtkWidget *titlebar; + + g_assert (DZL_IS_APPLICATION_WINDOW (self)); + + priv->fullscreen_source = 0; + + titlebar = dzl_application_window_get_titlebar (self); + + if (titlebar == NULL) + { + g_warning ("Attempt to alter fullscreen state without a titlebar set!"); + return G_SOURCE_REMOVE; + } + + /* + * This is where we apply the fullscreen widget transitions. + * + * If we were toggled really fast, we could have skipped oure previous + * operation. So make sure that the revealer is in the state we expect + * it before performing further (destructive) work. + */ + + g_object_ref (titlebar); + + if (priv->fullscreen) + { + /* Only listen for motion notify events while in fullscreen mode. */ + g_signal_handler_unblock (priv->event_box, priv->motion_notify_handler); + + if (titlebar && gtk_widget_is_ancestor (titlebar, GTK_WIDGET (priv->titlebar_container))) + { + revealer_set_reveal_child_now (priv->titlebar_revealer, FALSE); + gtk_container_remove (GTK_CONTAINER (priv->titlebar_container), titlebar); + gtk_container_add (GTK_CONTAINER (priv->titlebar_revealer), titlebar); + gtk_revealer_set_reveal_child (priv->titlebar_revealer, TRUE); + dzl_application_window_queue_dismissal (self); + } + } + else + { + /* Motion events are no longer needed */ + g_signal_handler_block (priv->event_box, priv->motion_notify_handler); + + if (gtk_widget_is_ancestor (titlebar, GTK_WIDGET (priv->titlebar_revealer))) + { + gtk_container_remove (GTK_CONTAINER (priv->titlebar_revealer), titlebar); + gtk_container_add (GTK_CONTAINER (priv->titlebar_container), titlebar); + revealer_set_reveal_child_now (priv->titlebar_revealer, FALSE); + } + } + + g_object_unref (titlebar); + + return G_SOURCE_REMOVE; +} + +static void +dzl_application_window_real_set_fullscreen (DzlApplicationWindow *self, + gboolean fullscreen) +{ + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + + g_assert (DZL_IS_APPLICATION_WINDOW (self)); + g_assert (priv->fullscreen != fullscreen); + + priv->fullscreen = fullscreen; + + if (priv->fullscreen_source != 0) + { + g_source_remove (priv->fullscreen_source); + priv->fullscreen_source = 0; + } + + if (priv->fullscreen) + { + /* + * Once we go fullscreen, the headerbar will no longer be visible. + * So we will delay for a short bit until we've likely entered the + * fullscreen state, and then remove the titlebar and place it into + * the hover state. + */ + priv->fullscreen_source = + gdk_threads_add_timeout_full (G_PRIORITY_LOW, + 300, + (GSourceFunc) dzl_application_window_complete_fullscreen, + self, NULL); + gtk_window_fullscreen (GTK_WINDOW (self)); + } + else + { + /* + * We must apply our unfullscreen state transition immediately + * so that we have a titlebar as soon as the window changes. + */ + dzl_application_window_complete_fullscreen (self); + gtk_window_unfullscreen (GTK_WINDOW (self)); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FULLSCREEN]); +} + +static void +dzl_application_window_set_focus (GtkWindow *window, + GtkWidget *widget) +{ + DzlApplicationWindow *self = (DzlApplicationWindow *)window; + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + GtkWidget *old_focus; + gboolean titlebar_had_focus = FALSE; + + g_assert (DZL_IS_APPLICATION_WINDOW (self)); + g_assert (!widget || GTK_IS_WIDGET (widget)); + + /* Check if we have titlebar focus already */ + old_focus = gtk_window_get_focus (window); + if (old_focus != NULL && + dzl_gtk_widget_is_ancestor_or_relative (old_focus, GTK_WIDGET (priv->titlebar_revealer))) + titlebar_had_focus = TRUE; + + /* Chain-up first */ + GTK_WINDOW_CLASS (dzl_application_window_parent_class)->set_focus (window, widget); + + /* Now see what is selected */ + widget = gtk_window_get_focus (window); + + if (widget != NULL) + { + if (dzl_gtk_widget_is_ancestor_or_relative (widget, GTK_WIDGET (priv->titlebar_revealer))) + { + /* Disable transition while the revealer is focused */ + if (priv->fullscreen_reveal_source != 0) + { + g_source_remove (priv->fullscreen_reveal_source); + priv->fullscreen_reveal_source = 0; + } + + /* If this was just focused, we might need to make it visible */ + gtk_revealer_set_reveal_child (priv->titlebar_revealer, TRUE); + } + else if (titlebar_had_focus) + { + /* We are going from titlebar to non-titlebar focus. Dismiss + * the titlebar immediately to get out of the users way. + */ + gtk_revealer_set_reveal_child (priv->titlebar_revealer, FALSE); + if (priv->fullscreen_reveal_source != 0) + { + g_source_remove (priv->fullscreen_reveal_source); + priv->fullscreen_reveal_source = 0; + } + } + } +} + +static void +dzl_application_window_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlApplicationWindow *self = (DzlApplicationWindow *)container; + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + + g_assert (DZL_IS_APPLICATION_WINDOW (self)); + g_assert (GTK_IS_WIDGET (widget)); + + gtk_container_add (GTK_CONTAINER (priv->event_box), widget); +} + +static gboolean +dzl_application_window_key_press_event (GtkWidget *widget, + GdkEventKey *event) +{ + DzlApplicationWindow *self = (DzlApplicationWindow *)widget; + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + gboolean ret; + + g_assert (DZL_IS_APPLICATION_WINDOW (self)); + g_assert (event != NULL); + + /* Be re-entrant safe from the shortcut manager */ + if (priv->in_key_press) + return GTK_WIDGET_CLASS (dzl_application_window_parent_class)->key_press_event (widget, event); + + priv->in_key_press = TRUE; + ret = dzl_shortcut_manager_handle_event (NULL, event, widget); + priv->in_key_press = FALSE; + + return ret; +} + +static void +dzl_application_window_destroy (GtkWidget *widget) +{ + DzlApplicationWindow *self = (DzlApplicationWindow *)widget; + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + + g_assert (DZL_IS_APPLICATION_WINDOW (self)); + + if (priv->event_box != NULL) + { + g_signal_handler_disconnect (priv->event_box, priv->motion_notify_handler); + priv->motion_notify_handler = 0; + } + + g_clear_pointer (&priv->titlebar_container, gtk_widget_destroy); + g_clear_pointer (&priv->titlebar_revealer, gtk_widget_destroy); + g_clear_pointer (&priv->event_box, gtk_widget_destroy); + g_clear_pointer (&priv->overlay, gtk_widget_destroy); + + if (priv->fullscreen_source) + { + g_source_remove (priv->fullscreen_source); + priv->fullscreen_source = 0; + } + + if (priv->fullscreen_reveal_source) + { + g_source_remove (priv->fullscreen_reveal_source); + priv->fullscreen_reveal_source = 0; + } + + GTK_WIDGET_CLASS (dzl_application_window_parent_class)->destroy (widget); +} + +static void +dzl_application_window_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlApplicationWindow *self = DZL_APPLICATION_WINDOW (object); + + switch (prop_id) + { + case PROP_FULLSCREEN: + g_value_set_boolean (value, dzl_application_window_get_fullscreen (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_application_window_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlApplicationWindow *self = DZL_APPLICATION_WINDOW (object); + + switch (prop_id) + { + case PROP_FULLSCREEN: + dzl_application_window_set_fullscreen (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_application_window_class_init (DzlApplicationWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + GtkWindowClass *window_class = GTK_WINDOW_CLASS (klass); + + object_class->get_property = dzl_application_window_get_property; + object_class->set_property = dzl_application_window_set_property; + + widget_class->destroy = dzl_application_window_destroy; + widget_class->key_press_event = dzl_application_window_key_press_event; + + container_class->add = dzl_application_window_add; + + window_class->set_focus = dzl_application_window_set_focus; + + klass->get_fullscreen = dzl_application_window_real_get_fullscreen; + klass->set_fullscreen = dzl_application_window_real_set_fullscreen; + + /** + * DzlApplicationWindow:fullscreen: + * + * The "fullscreen" property denotes if the window is in the fullscreen + * state. The titlebar of the #DzlApplicationWindow contains a revealer + * which will be repurposed into a floating bar while the window is in + * the fullscreen mode. + * + * Set this property to %FALSE to unfullscreen. + */ + properties [PROP_FULLSCREEN] = + g_param_spec_boolean ("fullscreen", + "Fullscreen", + "If the window is fullscreen", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_application_window_init (DzlApplicationWindow *self) +{ + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + g_autoptr(GPropertyAction) fullscreen = NULL; + + priv->titlebar_container = g_object_new (GTK_TYPE_STACK, + "name", "titlebar_container", + "visible", TRUE, + NULL); + g_signal_connect (priv->titlebar_container, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &priv->titlebar_container); + gtk_window_set_titlebar (GTK_WINDOW (self), GTK_WIDGET (priv->titlebar_container)); + + priv->overlay = g_object_new (GTK_TYPE_OVERLAY, + "visible", TRUE, + NULL); + g_signal_connect (priv->overlay, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &priv->overlay); + GTK_CONTAINER_CLASS (dzl_application_window_parent_class)->add (GTK_CONTAINER (self), + GTK_WIDGET (priv->overlay)); + + priv->event_box = g_object_new (GTK_TYPE_EVENT_BOX, + "above-child", FALSE, + "visible", TRUE, + "visible-window", TRUE, + NULL); + gtk_widget_set_events (GTK_WIDGET (priv->event_box), GDK_POINTER_MOTION_MASK); + g_signal_connect (priv->event_box, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &priv->event_box); + priv->motion_notify_handler = + g_signal_connect_swapped (priv->event_box, + "motion-notify-event", + G_CALLBACK (dzl_application_window_event_box_motion), + self); + g_signal_handler_block (priv->event_box, priv->motion_notify_handler); + gtk_container_add (GTK_CONTAINER (priv->overlay), GTK_WIDGET (priv->event_box)); + + priv->titlebar_revealer = g_object_new (GTK_TYPE_REVEALER, + "valign", GTK_ALIGN_START, + "hexpand", TRUE, + "transition-duration", 500, + "transition-type", GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN, + "reveal-child", TRUE, + "visible", TRUE, + NULL); + g_signal_connect (priv->titlebar_revealer, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &priv->titlebar_revealer); + gtk_overlay_add_overlay (priv->overlay, GTK_WIDGET (priv->titlebar_revealer)); + + fullscreen = g_property_action_new ("fullscreen", self, "fullscreen"); + g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (fullscreen)); +} + +/** + * dzl_application_window_get_fullscreen: + * @self: a #DzlApplicationWindow + * + * Gets if the window is in the fullscreen state. + * + * Returns: %TRUE if @self is fullscreen, otherwise %FALSE. + * + * Since: 3.26 + */ +gboolean +dzl_application_window_get_fullscreen (DzlApplicationWindow *self) +{ + g_return_val_if_fail (DZL_IS_APPLICATION_WINDOW (self), FALSE); + + return DZL_APPLICATION_WINDOW_GET_CLASS (self)->get_fullscreen (self); +} + +/** + * dzl_application_window_set_fullscreen: + * @self: a #DzlApplicationWindow + * @fullscreen: if the window should be in the fullscreen state + * + * Sets the #DzlApplicationWindow into either the fullscreen or unfullscreen + * state based on @fullscreen. + * + * The titlebar for the window is contained within a #GtkRevealer which is + * repurposed as a floating bar when the application is in fullscreen mode. + * + * See dzl_application_window_get_fullscreen() to get the current fullscreen + * state. + * + * Since: 3.26 + */ +void +dzl_application_window_set_fullscreen (DzlApplicationWindow *self, + gboolean fullscreen) +{ + g_return_if_fail (DZL_IS_APPLICATION_WINDOW (self)); + + fullscreen = !!fullscreen; + + if (fullscreen != dzl_application_window_get_fullscreen (self)) + DZL_APPLICATION_WINDOW_GET_CLASS (self)->set_fullscreen (self, fullscreen); +} + +static void +dzl_application_window_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + DzlApplicationWindow *self = (DzlApplicationWindow *)buildable; + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + + g_assert (DZL_IS_APPLICATION_WINDOW (self)); + g_assert (GTK_IS_BUILDER (builder)); + g_assert (G_IS_OBJECT (child)); + + if (g_strcmp0 (type, "titlebar") == 0) + gtk_container_add (GTK_CONTAINER (priv->titlebar_container), GTK_WIDGET (child)); + else + parent_buildable->add_child (buildable, builder, child, type); +} + +static void +buildable_iface_init (GtkBuildableIface *iface) +{ + parent_buildable = g_type_interface_peek_parent (iface); + + iface->add_child = dzl_application_window_add_child; +} + +/** + * dzl_application_window_get_titlebar: + * @self: a #DzlApplicationWindow + * + * Gets the titlebar for the window, if there is one. + * + * Returns: (transfer none): A #GtkWidget or %NULL + * + * Since: 3.26 + */ +GtkWidget * +dzl_application_window_get_titlebar (DzlApplicationWindow *self) +{ + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + GtkWidget *child; + + g_return_val_if_fail (DZL_IS_APPLICATION_WINDOW (self), NULL); + + child = gtk_stack_get_visible_child (priv->titlebar_container); + if (child == NULL) + child = gtk_bin_get_child (GTK_BIN (priv->titlebar_revealer)); + + return child; +} + +/** + * dzl_application_window_set_titlebar: + * @self: a #DzlApplicationWindow + * + * Sets the titlebar for the window. + * + * Generally, you want to do this from your GTK ui template by setting + * the <child type="titlebar"> + * + * Since: 3.26 + */ +void +dzl_application_window_set_titlebar (DzlApplicationWindow *self, + GtkWidget *titlebar) +{ + DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self); + + g_return_if_fail (DZL_IS_APPLICATION_WINDOW (self)); + g_return_if_fail (GTK_IS_WIDGET (titlebar)); + + if (titlebar != NULL) + gtk_container_add (GTK_CONTAINER (priv->titlebar_container), titlebar); +} diff --git a/src/app/dzl-application-window.h b/src/app/dzl-application-window.h new file mode 100644 index 0000000..d9bfe87 --- /dev/null +++ b/src/app/dzl-application-window.h @@ -0,0 +1,64 @@ +/* dzl-application-window.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_APPLICATION_WINDOW_H +#define DZL_APPLICATION_WINDOW_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_APPLICATION_WINDOW (dzl_application_window_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlApplicationWindow, dzl_application_window, DZL, APPLICATION_WINDOW, GtkApplicationWindow) + +struct _DzlApplicationWindowClass +{ + GtkApplicationWindowClass parent_class; + + gboolean (*get_fullscreen) (DzlApplicationWindow *self); + void (*set_fullscreen) (DzlApplicationWindow *self, + gboolean fullscreen); + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +gboolean dzl_application_window_get_fullscreen (DzlApplicationWindow *self); +DZL_AVAILABLE_IN_ALL +void dzl_application_window_set_fullscreen (DzlApplicationWindow *self, + gboolean fullscreen); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_application_window_get_titlebar (DzlApplicationWindow *self); +DZL_AVAILABLE_IN_ALL +void dzl_application_window_set_titlebar (DzlApplicationWindow *self, + GtkWidget *titlebar); + +G_END_DECLS + +#endif /* DZL_APPLICATION_WINDOW_H */ diff --git a/src/app/dzl-application.c b/src/app/dzl-application.c new file mode 100644 index 0000000..2e66f50 --- /dev/null +++ b/src/app/dzl-application.c @@ -0,0 +1,450 @@ +/* dzl-application.c + * + * Copyright (C) 2014-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-application" + +#include "config.h" + +#include "app/dzl-application.h" + +/** + * SECTION:dzl-application + * @title: DzlApplication + * @short_description: Application base class with goodies + * + * #DzlApplication is an extension of #GtkApplication with extra features to + * integrate various libdazzle subsystems with your application. We suggest + * subclassing #DzlApplication. + * + * The #DzlApplication class provides: + * + * - Automatic menu merging including the "app-menu". + * - Automatic Icon loading based on resources-base-path. + * - Automatic theme tracking to load CSS variants based on user themes. + * + * The #DzlApplication class automatically manages loading alternate CSS based + * on the active theme by tracking #GtkSettings:gtk-theme-name. Additionally, + * it supports menu merging including the base "app-menu" as loaded by automatic + * #GResources in #GApplication:resource-base-path. It will autom + */ + +typedef struct +{ + /* + * The theme manager is used to load CSS resources based on the + * GtkSettings:gtk-theme-name property. We add plugin resource + * paths to this to ensure that we load plugin CSS files too. + */ + DzlThemeManager *theme_manager; + + /* + * The menu manager deals with merging menu elements from multiple + * GtkBuilder files containing elements. We use the resource + * path to map to the merge_id in the hashtable. + */ + DzlMenuManager *menu_manager; + GHashTable *menu_merge_ids; + + /* + * The shortcut manager can be used to autoload keyboard themes from + * plugins or the application resources. + */ + DzlShortcutManager *shortcut_manager; + + /* + * Deferred resource loading. This can be used to call + * dzl_application_add_resources() before ::startup() has been called. Upon + * ::startup(), we'll apply these. If this is set to NULL, ::startup() has + * already been called. + */ + GPtrArray *deferred_resources; + +} DzlApplicationPrivate; + +enum { + PROP_0, + PROP_MENU_MANAGER, + PROP_SHORTCUT_MANAGER, + PROP_THEME_MANAGER, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlApplication, dzl_application, GTK_TYPE_APPLICATION) + +static void +dzl_application_real_add_resources (DzlApplication *self, + const gchar *resource_path) +{ + DzlApplicationPrivate *priv = dzl_application_get_instance_private (self); + g_autoptr(GError) error = NULL; + g_autofree gchar *menu_path = NULL; + g_autofree gchar *keythemes_path = NULL; + guint merge_id; + + g_assert (DZL_IS_APPLICATION (self)); + g_assert (resource_path != NULL); + + /* We use interned strings for hash table keys */ + resource_path = g_intern_string (resource_path); + + /* + * Allow the theme manager to monitor the css/Adwaita.css or other themes + * based on gtk-theme-name. The theme manager also loads icons. + */ + dzl_theme_manager_add_resources (priv->theme_manager, resource_path); + + /* + * If the resource path has a gtk/menus.ui file, we want to auto-load and + * merge the menus. + */ + menu_path = g_build_filename (resource_path, "gtk", "menus.ui", NULL); + + if (g_str_has_prefix (menu_path, "resource://")) + merge_id = dzl_menu_manager_add_resource (priv->menu_manager, menu_path, &error); + else + merge_id = dzl_menu_manager_add_filename (priv->menu_manager, menu_path, &error); + + if (merge_id != 0) + g_hash_table_insert (priv->menu_merge_ids, (gchar *)resource_path, GUINT_TO_POINTER (merge_id)); + + if (error != NULL && + !(g_error_matches (error, G_RESOURCE_ERROR, G_RESOURCE_ERROR_NOT_FOUND) || + g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))) + g_warning ("%s", error->message); + + /* + * Load any shortcut theme information from the plugin or application + * resources. We always append so that the application resource dir is + * loaded before any plugin paths. + */ + keythemes_path = g_build_filename (resource_path, "shortcuts", NULL); + dzl_shortcut_manager_append_search_path (priv->shortcut_manager, keythemes_path); +} + +static void +dzl_application_real_remove_resources (DzlApplication *self, + const gchar *resource_path) +{ + DzlApplicationPrivate *priv = dzl_application_get_instance_private (self); + g_autofree gchar *keythemes_path = NULL; + guint merge_id; + + g_assert (DZL_IS_APPLICATION (self)); + g_assert (resource_path != NULL); + + /* We use interned strings for hash table lookups */ + resource_path = g_intern_string (resource_path); + + /* Remove any loaded CSS providers for @resource_path/css/. */ + dzl_theme_manager_remove_resources (priv->theme_manager, resource_path); + + /* Remove any merged menus from the @resource_path/gtk/menus.ui */ + merge_id = GPOINTER_TO_UINT (g_hash_table_lookup (priv->menu_merge_ids, resource_path)); + if (merge_id != 0) + { + if (g_hash_table_contains (priv->menu_merge_ids, resource_path)) + g_hash_table_remove (priv->menu_merge_ids, resource_path); + dzl_menu_manager_remove (priv->menu_manager, merge_id); + } + + /* Remove keythemes path from the shortcuts manager */ + keythemes_path = g_strjoin (NULL, "resource://", resource_path, "/shortcuts", NULL); + dzl_shortcut_manager_remove_search_path (priv->shortcut_manager, keythemes_path); +} + +static void +dzl_application_startup (GApplication *app) +{ + DzlApplication *self = (DzlApplication *)app; + DzlApplicationPrivate *priv = dzl_application_get_instance_private (self); + const gchar *resource_base_path; + GMenu *app_menu; + + g_assert (DZL_IS_APPLICATION (self)); + + G_APPLICATION_CLASS (dzl_application_parent_class)->startup (app); + + /* + * We cannot register resources before chaining startup because + * the GtkSettings and other plumbing will not yet be initialized. + */ + + /* Register our resources that are part of libdazzle. */ + DZL_APPLICATION_GET_CLASS (self)->add_resources (self, "resource:///org/gnome/dazzle/"); + + /* Now register the application resources */ + if (NULL != (resource_base_path = g_application_get_resource_base_path (app))) + { + g_autofree gchar *resource_path = NULL; + + resource_path = g_strdup_printf ("resource://%s", resource_base_path); + DZL_APPLICATION_GET_CLASS (self)->add_resources (self, resource_path); + } + + /* If the application has "app-menu" defined in menus.ui, we want to + * assign it to the application. If we used the base GtkApplication for + * menus, this would be done for us. But since we are doing menu merging, + * we need to do it manually. + */ + app_menu = dzl_menu_manager_get_menu_by_id (priv->menu_manager, "app-menu"); + gtk_application_set_app_menu (GTK_APPLICATION (self), G_MENU_MODEL (app_menu)); + + /* + * Now apply our deferred resources. + */ + for (guint i = 0; i < priv->deferred_resources->len; i++) + { + const gchar *path = g_ptr_array_index (priv->deferred_resources, i); + DZL_APPLICATION_GET_CLASS (self)->add_resources (self, path); + } + g_clear_pointer (&priv->deferred_resources, g_ptr_array_unref); + + /* + * Now force reload the keyboard shortcuts without defering to the main + * loop or anything. + */ + dzl_shortcut_manager_reload (priv->shortcut_manager, NULL); +} + +static void +dzl_application_finalize (GObject *object) +{ + DzlApplication *self = (DzlApplication *)object; + DzlApplicationPrivate *priv = dzl_application_get_instance_private (self); + + g_clear_pointer (&priv->deferred_resources, g_ptr_array_unref); + g_clear_pointer (&priv->menu_merge_ids, g_hash_table_unref); + g_clear_object (&priv->theme_manager); + g_clear_object (&priv->menu_manager); + g_clear_object (&priv->shortcut_manager); + + G_OBJECT_CLASS (dzl_application_parent_class)->finalize (object); +} + +static void +dzl_application_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlApplication *self = DZL_APPLICATION (object); + + switch (prop_id) + { + case PROP_MENU_MANAGER: + g_value_set_object (value, dzl_application_get_menu_manager (self)); + break; + + case PROP_SHORTCUT_MANAGER: + g_value_set_object (value, dzl_application_get_shortcut_manager (self)); + break; + + case PROP_THEME_MANAGER: + g_value_set_object (value, dzl_application_get_theme_manager (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_application_class_init (DzlApplicationClass *klass) +{ + GApplicationClass *g_app_class = G_APPLICATION_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_application_finalize; + object_class->get_property = dzl_application_get_property; + + g_app_class->startup = dzl_application_startup; + + klass->add_resources = dzl_application_real_add_resources; + klass->remove_resources = dzl_application_real_remove_resources; + + properties [PROP_MENU_MANAGER] = + g_param_spec_object ("menu-manager", NULL, NULL, + DZL_TYPE_MENU_MANAGER, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHORTCUT_MANAGER] = + g_param_spec_object ("shortcut-manager", NULL, NULL, + DZL_TYPE_SHORTCUT_MANAGER, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_THEME_MANAGER] = + g_param_spec_object ("theme-manager", NULL, NULL, + DZL_TYPE_THEME_MANAGER, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_application_init (DzlApplication *self) +{ + DzlApplicationPrivate *priv = dzl_application_get_instance_private (self); + g_autoptr(GPropertyAction) shortcut_theme_action = NULL; + + g_application_set_default (G_APPLICATION (self)); + + priv->deferred_resources = g_ptr_array_new (); + priv->theme_manager = dzl_theme_manager_new (); + priv->menu_manager = dzl_menu_manager_new (); + priv->menu_merge_ids = g_hash_table_new (NULL, NULL); + priv->shortcut_manager = g_object_ref (dzl_shortcut_manager_get_default ()); + + shortcut_theme_action = g_property_action_new ("shortcut-theme", priv->shortcut_manager, "theme-name"); + g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (shortcut_theme_action)); +} + +/** + * dzl_application_get_menu_manager: + * @self: a #DzlApplication + * + * Gets the menu manager for the application. + * + * Returns: (transfer none): A #DzlMenuManager + */ +DzlMenuManager * +dzl_application_get_menu_manager (DzlApplication *self) +{ + DzlApplicationPrivate *priv = dzl_application_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_APPLICATION (self), NULL); + + return priv->menu_manager; +} + +/** + * dzl_application_get_theme_manager: + * @self: a #DzlApplication + * + * Get the theme manager for the application. + * + * Returns: (transfer none): A #DzlThemeManager + */ +DzlThemeManager * +dzl_application_get_theme_manager (DzlApplication *self) +{ + DzlApplicationPrivate *priv = dzl_application_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_APPLICATION (self), NULL); + + return priv->theme_manager; +} + +/** + * dzl_application_get_menu_by_id: + * @self: a #DzlApplication + * @menu_id: the id of the menu to locate + * + * Similar to gtk_application_get_menu_by_id() but takes into account + * menu merging which could have occurred upon loading plugins. + * + * Returns: (transfer none): A #GMenu + */ +GMenu * +dzl_application_get_menu_by_id (DzlApplication *self, + const gchar *menu_id) +{ + DzlApplicationPrivate *priv = dzl_application_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_APPLICATION (self), NULL); + g_return_val_if_fail (menu_id != NULL, NULL); + + return dzl_menu_manager_get_menu_by_id (priv->menu_manager, menu_id); +} + +/** + * dzl_application_add_resources: + * @self: a #DzlApplication + * @resource_path: the location of the resources. + * + * This adds @resource_path to the list of "automatic resources". + * + * If @resource_path starts with "resource://", then the corresponding + * #GResources path will be searched for resources. Otherwise, @resource_path + * should be a path to a location on disk. + * + * The #DzlApplication will locate resources such as CSS themes, icons, and + * keyboard shortcuts using @resource_path. + */ +void +dzl_application_add_resources (DzlApplication *self, + const gchar *resource_path) +{ + DzlApplicationPrivate *priv = dzl_application_get_instance_private (self); + + g_return_if_fail (DZL_IS_APPLICATION (self)); + g_return_if_fail (resource_path != NULL); + + if (priv->deferred_resources != NULL) + { + g_ptr_array_add (priv->deferred_resources, (gpointer)g_intern_string (resource_path)); + return; + } + + DZL_APPLICATION_GET_CLASS (self)->add_resources (self, resource_path); +} + +/** + * dzl_application_remove_resources: + * @self: a #DzlApplication + * @resource_path: the location of the resources. + * + * This attempts to undo as many side-effects as possible from a call to + * dzl_application_add_resources(). + */ +void +dzl_application_remove_resources (DzlApplication *self, + const gchar *resource_path) +{ + DzlApplicationPrivate *priv = dzl_application_get_instance_private (self); + + g_return_if_fail (DZL_IS_APPLICATION (self)); + g_return_if_fail (resource_path != NULL); + + if (priv->deferred_resources != NULL) + { + g_ptr_array_remove (priv->deferred_resources, (gpointer)g_intern_string (resource_path)); + return; + } + + DZL_APPLICATION_GET_CLASS (self)->remove_resources (self, resource_path); +} + +/** + * dzl_application_get_shortcut_manager: + * @self: a #DzlApplication + * + * Gets the #DzlShortcutManager for the application. + * + * Returns: (transfer none): A #DzlShortcutManager + */ +DzlShortcutManager * +dzl_application_get_shortcut_manager (DzlApplication *self) +{ + DzlApplicationPrivate *priv = dzl_application_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_APPLICATION (self), NULL); + + return priv->shortcut_manager; +} diff --git a/src/app/dzl-application.h b/src/app/dzl-application.h new file mode 100644 index 0000000..cd26976 --- /dev/null +++ b/src/app/dzl-application.h @@ -0,0 +1,79 @@ +/* dzl-application.h + * + * Copyright (C) 2014-2017 Christian Hergert + * + * 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 . + */ + +#if !defined (DAZZLE_INSIDE) && !defined (DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_APPLICATION_H +#define DZL_APPLICATION_H + +#include + +#include "dzl-version-macros.h" + +#include "menus/dzl-menu-manager.h" +#include "shortcuts/dzl-shortcut-manager.h" +#include "theming/dzl-theme-manager.h" + +G_BEGIN_DECLS + +#define DZL_APPLICATION_DEFAULT (DZL_APPLICATION (g_application_get_default ())) +#define DZL_TYPE_APPLICATION (dzl_application_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlApplication, dzl_application, DZL, APPLICATION, GtkApplication) + +struct _DzlApplicationClass +{ + GtkApplicationClass parent_class; + + void (*add_resources) (DzlApplication *self, + const gchar *resource_path); + void (*remove_resources) (DzlApplication *self, + const gchar *resource_path); + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +DzlMenuManager *dzl_application_get_menu_manager (DzlApplication *self); +DZL_AVAILABLE_IN_ALL +DzlShortcutManager *dzl_application_get_shortcut_manager (DzlApplication *self); +DZL_AVAILABLE_IN_ALL +DzlThemeManager *dzl_application_get_theme_manager (DzlApplication *self); +DZL_AVAILABLE_IN_ALL +GMenu *dzl_application_get_menu_by_id (DzlApplication *self, + const gchar *menu_id); +DZL_AVAILABLE_IN_ALL +void dzl_application_add_resources (DzlApplication *self, + const gchar *resource_path); +DZL_AVAILABLE_IN_ALL +void dzl_application_remove_resources (DzlApplication *self, + const gchar *resource_path); + +G_END_DECLS + +#endif /* DZL_APPLICATION_H */ diff --git a/src/app/meson.build b/src/app/meson.build new file mode 100644 index 0000000..363569c --- /dev/null +++ b/src/app/meson.build @@ -0,0 +1,14 @@ +app_headers = [ + 'dzl-application.h', + 'dzl-application-window.h', +] + +app_sources = [ + 'dzl-application.c', + 'dzl-application-window.c', +] + +libdazzle_public_headers += files(app_headers) +libdazzle_public_sources += files(app_sources) + +install_headers(app_headers, subdir: join_paths(libdazzle_header_subdir, 'app')) diff --git a/src/bindings/dzl-binding-group.c b/src/bindings/dzl-binding-group.c new file mode 100644 index 0000000..03590ac --- /dev/null +++ b/src/bindings/dzl-binding-group.c @@ -0,0 +1,640 @@ +/* dzl-binding-group.c + * + * Copyright (C) 2015 Christian Hergert + * Copyright (C) 2015 Garrett Regier + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-binding-group" + +#include "config.h" + +#include + +#include "dzl-binding-group.h" + +/** + * SECTION:dzlbindinggroup + * @title: DzlBindingGroup + * @short_description: Manage a collection of #GBindings on + * a #GObject as a group. + * + * #DzlBindingGroup manages to simplify the process of binding + * many properties from a #GObject as a group. As such there is no API + * to unbind a property from the group. + * + * In particular, this allows you to change the source instance for the + * bindings. This automatically causes the unbinding of the properties + * from the old instance and binding to the new instance. + * + * This should not be confused with #GtkBindingGroup. + */ + +struct _DzlBindingGroup +{ + GObject parent_instance; + + GObject *source; + GPtrArray *lazy_bindings; +}; + +typedef struct +{ + DzlBindingGroup *group; + const gchar *source_property; + const gchar *target_property; + GObject *target; + GBinding *binding; + gpointer user_data; + GDestroyNotify user_data_destroy; + gpointer transform_to; + gpointer transform_from; + GBindingFlags binding_flags; + guint using_closures : 1; +} LazyBinding; + +G_DEFINE_TYPE (DzlBindingGroup, dzl_binding_group, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_SOURCE, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +/*#define DEBUG_BINDINGS 1*/ + +#ifdef DEBUG_BINDINGS +static gchar * +_g_flags_to_string (GFlagsClass *flags_class, + guint value) +{ + GString *str; + GFlagsValue *flags_value; + gboolean first = TRUE; + + str = g_string_new (NULL); + + while ((first || value != 0) && + (flags_value = g_flags_get_first_value (flags_class, value)) != NULL) + { + if (!first) + g_string_append (str, " | "); + + g_string_append (str, flags_value->value_name); + + first = FALSE; + value &= ~(flags_value->value); + } + + return g_string_free (str, FALSE); +} +#endif + +static void +dzl_binding_group_connect (DzlBindingGroup *self, + LazyBinding *lazy_binding) +{ + GBinding *binding; + + g_assert (DZL_IS_BINDING_GROUP (self)); + g_assert (self->source != NULL); + g_assert (lazy_binding != NULL); + g_assert (lazy_binding->binding == NULL); + g_assert (lazy_binding->target != NULL); + g_assert (lazy_binding->target_property != NULL); + g_assert (lazy_binding->source_property != NULL); + +#ifdef DEBUG_BINDINGS + { + GFlagsClass *flags_class; + g_autofree gchar *flags_str = NULL; + + flags_class = g_type_class_ref (G_TYPE_BINDING_FLAGS); + flags_str = _g_flags_to_string (flags_class, + lazy_binding->binding_flags); + + g_print ("Binding %s(%p):%s to %s(%p):%s (flags=%s)\n", + G_OBJECT_TYPE_NAME (self->source), + self->source, + lazy_binding->source_property, + G_OBJECT_TYPE_NAME (lazy_binding->target), + lazy_binding->target, + lazy_binding->target_property, + flags_str); + + g_type_class_unref (flags_class); + } +#endif + + if (!lazy_binding->using_closures) + { + binding = g_object_bind_property_full (self->source, + lazy_binding->source_property, + lazy_binding->target, + lazy_binding->target_property, + lazy_binding->binding_flags, + lazy_binding->transform_to, + lazy_binding->transform_from, + lazy_binding->user_data, + NULL); + } + else + { + binding = g_object_bind_property_with_closures (self->source, + lazy_binding->source_property, + lazy_binding->target, + lazy_binding->target_property, + lazy_binding->binding_flags, + lazy_binding->transform_to, + lazy_binding->transform_from); + } + + lazy_binding->binding = binding; +} + +static void +dzl_binding_group_disconnect (LazyBinding *lazy_binding) +{ + g_assert (lazy_binding != NULL); + + if (lazy_binding->binding != NULL) + { + g_binding_unbind (lazy_binding->binding); + lazy_binding->binding = NULL; + } +} + +static void +dzl_binding_group__source_weak_notify (gpointer data, + GObject *where_object_was) +{ + DzlBindingGroup *self = data; + gsize i; + + g_assert (DZL_IS_BINDING_GROUP (self)); + + self->source = NULL; + + for (i = 0; i < self->lazy_bindings->len; i++) + { + LazyBinding *lazy_binding; + + lazy_binding = g_ptr_array_index (self->lazy_bindings, i); + lazy_binding->binding = NULL; + } +} + +static void +dzl_binding_group__target_weak_notify (gpointer data, + GObject *where_object_was) +{ + DzlBindingGroup *self = data; + gsize i; + + g_assert (DZL_IS_BINDING_GROUP (self)); + + for (i = 0; i < self->lazy_bindings->len; i++) + { + LazyBinding *lazy_binding; + + lazy_binding = g_ptr_array_index (self->lazy_bindings, i); + + if (lazy_binding->target == where_object_was) + { + lazy_binding->target = NULL; + lazy_binding->binding = NULL; + + g_ptr_array_remove_index_fast (self->lazy_bindings, i); + break; + } + } +} + +static void +lazy_binding_free (gpointer data) +{ + LazyBinding *lazy_binding = data; + + if (lazy_binding->target != NULL) + { + g_object_weak_unref (lazy_binding->target, + dzl_binding_group__target_weak_notify, + lazy_binding->group); + lazy_binding->target = NULL; + } + + dzl_binding_group_disconnect (lazy_binding); + + lazy_binding->group = NULL; + lazy_binding->source_property = NULL; + lazy_binding->target_property = NULL; + + if (lazy_binding->user_data_destroy) + lazy_binding->user_data_destroy (lazy_binding->user_data); + + if (lazy_binding->using_closures) + { + g_clear_pointer (&lazy_binding->transform_to, g_closure_unref); + g_clear_pointer (&lazy_binding->transform_from, g_closure_unref); + } + + g_slice_free (LazyBinding, lazy_binding); +} + +static void +dzl_binding_group_dispose (GObject *object) +{ + DzlBindingGroup *self = (DzlBindingGroup *)object; + + g_assert (DZL_IS_BINDING_GROUP (self)); + + if (self->source != NULL) + { + g_object_weak_unref (self->source, + dzl_binding_group__source_weak_notify, + self); + self->source = NULL; + } + + if (self->lazy_bindings->len != 0) + g_ptr_array_remove_range (self->lazy_bindings, 0, self->lazy_bindings->len); + + G_OBJECT_CLASS (dzl_binding_group_parent_class)->dispose (object); +} + +static void +dzl_binding_group_finalize (GObject *object) +{ + DzlBindingGroup *self = (DzlBindingGroup *)object; + + g_assert (self->lazy_bindings != NULL); + g_assert (self->lazy_bindings->len == 0); + + g_clear_pointer (&self->lazy_bindings, g_ptr_array_unref); + + G_OBJECT_CLASS (dzl_binding_group_parent_class)->finalize (object); +} + +static void +dzl_binding_group_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlBindingGroup *self = DZL_BINDING_GROUP (object); + + switch (prop_id) + { + case PROP_SOURCE: + g_value_set_object (value, dzl_binding_group_get_source (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_binding_group_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlBindingGroup *self = DZL_BINDING_GROUP (object); + + switch (prop_id) + { + case PROP_SOURCE: + dzl_binding_group_set_source (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_binding_group_class_init (DzlBindingGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = dzl_binding_group_dispose; + object_class->finalize = dzl_binding_group_finalize; + object_class->get_property = dzl_binding_group_get_property; + object_class->set_property = dzl_binding_group_set_property; + + /** + * DzlBindingGroup:source + * + * The source object used for binding properties. + */ + properties [PROP_SOURCE] = + g_param_spec_object ("source", + "Source", + "The source GObject used for binding properties.", + G_TYPE_OBJECT, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_binding_group_init (DzlBindingGroup *self) +{ + self->lazy_bindings = g_ptr_array_new_with_free_func (lazy_binding_free); +} + +/** + * dzl_binding_group_new: + * + * Creates a new #DzlBindingGroup. + * + * Returns: a new #DzlBindingGroup + */ +DzlBindingGroup * +dzl_binding_group_new (void) +{ + return g_object_new (DZL_TYPE_BINDING_GROUP, NULL); +} + +/** + * dzl_binding_group_get_source: + * @self: the #DzlBindingGroup + * + * Gets the source object used for binding properties. + * + * Returns: (transfer none) (nullable): the source object. + */ +GObject * +dzl_binding_group_get_source (DzlBindingGroup *self) +{ + g_return_val_if_fail (DZL_IS_BINDING_GROUP (self), NULL); + + return self->source; +} + +static gboolean +dzl_binding_group_check_source (DzlBindingGroup *self, + gpointer source) +{ + gsize i; + + for (i = 0; i < self->lazy_bindings->len; i++) + { + LazyBinding *lazy_binding; + + lazy_binding = g_ptr_array_index (self->lazy_bindings, i); + + g_return_val_if_fail (g_object_class_find_property (G_OBJECT_GET_CLASS (source), + lazy_binding->source_property) != NULL, + FALSE); + } + + return TRUE; +} + +/** + * dzl_binding_group_set_source: + * @self: the #DzlBindingGroup + * @source: (type GObject) (nullable): the source #GObject + * + * Sets @source as the source object used for creating property + * bindings. If there is already a source object all bindings from it + * will be removed. + * + * Note: All properties that have been bound must exist on @source. + */ +void +dzl_binding_group_set_source (DzlBindingGroup *self, + gpointer source) +{ + g_return_if_fail (DZL_IS_BINDING_GROUP (self)); + g_return_if_fail (!source || G_IS_OBJECT (source)); + g_return_if_fail (source != (gpointer)self); + + if (source == (gpointer)self->source) + return; + + if (self->source != NULL) + { + gsize i; + + g_object_weak_unref (self->source, + dzl_binding_group__source_weak_notify, + self); + self->source = NULL; + + for (i = 0; i < self->lazy_bindings->len; i++) + { + LazyBinding *lazy_binding; + + lazy_binding = g_ptr_array_index (self->lazy_bindings, i); + dzl_binding_group_disconnect (lazy_binding); + } + } + + if (source != NULL && dzl_binding_group_check_source (self, source)) + { + gsize i; + + self->source = source; + g_object_weak_ref (self->source, + dzl_binding_group__source_weak_notify, + self); + + for (i = 0; i < self->lazy_bindings->len; i++) + { + LazyBinding *lazy_binding; + + lazy_binding = g_ptr_array_index (self->lazy_bindings, i); + dzl_binding_group_connect (self, lazy_binding); + } + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SOURCE]); +} + +void +dzl_binding_group_bind_helper (DzlBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags, + gpointer transform_to, + gpointer transform_from, + gpointer user_data, + GDestroyNotify user_data_destroy, + gboolean using_closures) +{ + LazyBinding *lazy_binding; + + g_return_if_fail (DZL_IS_BINDING_GROUP (self)); + g_return_if_fail (source_property != NULL); + g_return_if_fail (self->source == NULL || + g_object_class_find_property (G_OBJECT_GET_CLASS (self->source), + source_property) != NULL); + g_return_if_fail (G_IS_OBJECT (target)); + g_return_if_fail (target_property != NULL); + g_return_if_fail (g_object_class_find_property (G_OBJECT_GET_CLASS (target), + target_property) != NULL); + g_return_if_fail (target != (gpointer)self || + strcmp (source_property, target_property) != 0); + + lazy_binding = g_slice_new0 (LazyBinding); + lazy_binding->group = self; + lazy_binding->source_property = g_intern_string (source_property); + lazy_binding->target_property = g_intern_string (target_property); + lazy_binding->target = target; + lazy_binding->binding_flags = flags | G_BINDING_SYNC_CREATE; + lazy_binding->user_data = user_data; + lazy_binding->user_data_destroy = user_data_destroy; + lazy_binding->transform_to = transform_to; + lazy_binding->transform_from = transform_from; + + if (using_closures) + { + lazy_binding->using_closures = TRUE; + + if (transform_to != NULL) + g_closure_sink (g_closure_ref (transform_to)); + + if (transform_from != NULL) + g_closure_sink (g_closure_ref (transform_from)); + } + + g_object_weak_ref (target, + dzl_binding_group__target_weak_notify, + self); + + g_ptr_array_add (self->lazy_bindings, lazy_binding); + + if (self->source != NULL) + dzl_binding_group_connect (self, lazy_binding); +} + +/** + * dzl_binding_group_bind: + * @self: the #DzlBindingGroup + * @source_property: the property on the source to bind + * @target: (type GObject): the target #GObject + * @target_property: the property on @target to bind + * @flags: the flags used to create the #GBinding + * + * Creates a binding between @source_property on the source object + * and @target_property on @target. Whenever the @source_property + * is changed the @target_property is updated using the same value. + * The binding flags #G_BINDING_SYNC_CREATE is automatically specified. + * + * See: g_object_bind_property(). + */ +void +dzl_binding_group_bind (DzlBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags) +{ + dzl_binding_group_bind_full (self, source_property, + target, target_property, + flags, + NULL, NULL, + NULL, NULL); +} + +/** + * dzl_binding_group_bind_full: + * @self: the #DzlBindingGroup + * @source_property: the property on the source to bind + * @target: (type GObject): the target #GObject + * @target_property: the property on @target to bind + * @flags: the flags used to create the #GBinding + * @transform_to: (scope notified) (nullable): the transformation function + * from the source object to the @target, or %NULL to use the default + * @transform_from: (scope notified) (nullable): the transformation function + * from the @target to the source object, or %NULL to use the default + * @user_data: custom data to be passed to the transformation + * functions, or %NULL + * @user_data_destroy: function to be called when disposing the binding, + * to free the resources used by the transformation functions + * + * Creates a binding between @source_property on the source object and + * @target_property on @target, allowing you to set the transformation + * functions to be used by the binding. The binding flags + * #G_BINDING_SYNC_CREATE is automatically specified. + * + * See: g_object_bind_property_full(). + */ +void +dzl_binding_group_bind_full (DzlBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags, + GBindingTransformFunc transform_to, + GBindingTransformFunc transform_from, + gpointer user_data, + GDestroyNotify user_data_destroy) +{ + dzl_binding_group_bind_helper (self, source_property, + target, target_property, + flags, + transform_to, transform_from, + user_data, user_data_destroy, + FALSE); +} + +/** + * dzl_binding_group_bind_with_closures: (rename-to dzl_binding_group_bind_full) + * @self: the #DzlBindingGroup + * @source_property: the property on the source to bind + * @target: (type GObject): the target #GObject + * @target_property: the property on @target to bind + * @flags: the flags used to create the #GBinding + * @transform_to: (nullable): a #GClosure wrapping the + * transformation function from the source object to the @target, + * or %NULL to use the default + * @transform_from: (nullable): a #GClosure wrapping the + * transformation function from the @target to the source object, + * or %NULL to use the default + * + * Creates a binding between @source_property on the source object and + * @target_property on @target, allowing you to set the transformation + * functions to be used by the binding. The binding flags + * #G_BINDING_SYNC_CREATE is automatically specified. + * + * This function is the language bindings friendly version of + * dzl_binding_group_bind_property_full(), using #GClosures + * instead of function pointers. + * + * See: g_object_bind_property_with_closures(). + */ +void +dzl_binding_group_bind_with_closures (DzlBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags, + GClosure *transform_to, + GClosure *transform_from) +{ + dzl_binding_group_bind_helper (self, source_property, + target, target_property, + flags, + transform_to, transform_from, + NULL, NULL, + TRUE); +} diff --git a/src/bindings/dzl-binding-group.h b/src/bindings/dzl-binding-group.h new file mode 100644 index 0000000..812abf1 --- /dev/null +++ b/src/bindings/dzl-binding-group.h @@ -0,0 +1,68 @@ +/* dzl-binding-group.h + * + * Copyright (C) 2015 Christian Hergert + * Copyright (C) 2015 Garrett Regier + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_BINDING_GROUP_H +#define DZL_BINDING_GROUP_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_BINDING_GROUP (dzl_binding_group_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlBindingGroup, dzl_binding_group, DZL, BINDING_GROUP, GObject) + +DZL_AVAILABLE_IN_ALL +DzlBindingGroup *dzl_binding_group_new (void); +DZL_AVAILABLE_IN_ALL +GObject *dzl_binding_group_get_source (DzlBindingGroup *self); +DZL_AVAILABLE_IN_ALL +void dzl_binding_group_set_source (DzlBindingGroup *self, + gpointer source); +DZL_AVAILABLE_IN_ALL +void dzl_binding_group_bind (DzlBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags); +DZL_AVAILABLE_IN_ALL +void dzl_binding_group_bind_full (DzlBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags, + GBindingTransformFunc transform_to, + GBindingTransformFunc transform_from, + gpointer user_data, + GDestroyNotify user_data_destroy); +DZL_AVAILABLE_IN_ALL +void dzl_binding_group_bind_with_closures (DzlBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags, + GClosure *transform_to, + GClosure *transform_from); + +G_END_DECLS + +#endif /* DZL_BINDING_GROUP_H */ diff --git a/src/bindings/dzl-signal-group.c b/src/bindings/dzl-signal-group.c new file mode 100644 index 0000000..5c30b76 --- /dev/null +++ b/src/bindings/dzl-signal-group.c @@ -0,0 +1,830 @@ +/* dzl-signal-group.c + * + * Copyright (C) 2015 Christian Hergert + * Copyright (C) 2015 Garrett Regier + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-signal-group" + +#include "config.h" + +#include + +#include "dzl-signal-group.h" + +/** + * SECTION:dzlsignalgroup + * @title: DzlSignalGroup + * @short_description: Manage a collection of signals on a #GObject + * + * #DzlSignalGroup manages to simplify the process of connecting + * many signals to a #GObject as a group. As such there is no API + * to disconnect a signal from the group. + * + * In particular, this allows you to: + * + * - Change the target instance, which automatically causes disconnection + * of the signals from the old instance and connecting to the new instance. + * - Block and unblock signals as a group + * - Ensuring that blocked state transfers across target instances. + * + * One place you might want to use such a structure is with #GtkTextView and + * #GtkTextBuffer. Often times, you'll need to connect to many signals on + * #GtkTextBuffer from a #GtkTextView subclass. This allows you to create a + * signal group during instance construction, simply bind the + * #GtkTextView:buffer property to #DzlSignalGroup:target and connect + * all the signals you need. When the #GtkTextView:buffer property changes + * all of the signals will be transitioned correctly. + */ + +struct _DzlSignalGroup +{ + GObject parent_instance; + + GWeakRef target_ref; + GPtrArray *handlers; + GType target_type; + gsize block_count; + + guint has_bound_at_least_once : 1; +}; + +struct _DzlSignalGroupClass +{ + GObjectClass parent_class; + + void (*bind) (DzlSignalGroup *self, + GObject *target); +}; + +typedef struct +{ + DzlSignalGroup *group; + gulong handler_id; + GClosure *closure; + guint signal_id; + GQuark signal_detail; + guint connect_after : 1; +} SignalHandler; + +G_DEFINE_TYPE (DzlSignalGroup, dzl_signal_group, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_TARGET, + PROP_TARGET_TYPE, + LAST_PROP +}; + +enum { + BIND, + UNBIND, + LAST_SIGNAL +}; + +static GParamSpec *properties [LAST_PROP]; +static guint signals [LAST_SIGNAL]; + +static void +dzl_signal_group_set_target_type (DzlSignalGroup *self, + GType target_type) +{ + g_assert (DZL_IS_SIGNAL_GROUP (self)); + g_assert (g_type_is_a (target_type, G_TYPE_OBJECT)); + + self->target_type = target_type; + + /* The class must be created at least once for the signals + * to be registered, otherwise g_signal_parse_name() will fail + */ + if (G_TYPE_IS_INTERFACE (target_type)) + { + if (g_type_default_interface_peek (target_type) == NULL) + g_type_default_interface_unref (g_type_default_interface_ref (target_type)); + } + else + { + if (g_type_class_peek (target_type) == NULL) + g_type_class_unref (g_type_class_ref (target_type)); + } +} + +static void +dzl_signal_group_gc_handlers (DzlSignalGroup *self) +{ + g_assert (DZL_IS_SIGNAL_GROUP (self)); + + /* + * Remove any handlers for which the closures have become invalid. We do + * this cleanup lazily to avoid situations where we could have disposal + * active on both the signal group and the peer object. + */ + + for (guint i = self->handlers->len; i > 0; i--) + { + const SignalHandler *handler = g_ptr_array_index (self->handlers, i - 1); + + g_assert (handler != NULL); + g_assert (handler->closure != NULL); + + if (handler->closure->is_invalid) + g_ptr_array_remove_index (self->handlers, i - 1); + } +} + +static void +dzl_signal_group__target_weak_notify (gpointer data, + GObject *where_object_was) +{ + DzlSignalGroup *self = data; + + g_assert (DZL_IS_SIGNAL_GROUP (self)); + g_assert (where_object_was != NULL); + + g_weak_ref_set (&self->target_ref, NULL); + + for (guint i = 0; i < self->handlers->len; i++) + { + SignalHandler *handler = g_ptr_array_index (self->handlers, i); + + handler->handler_id = 0; + } + + g_signal_emit (self, signals [UNBIND], 0); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TARGET]); +} + +static void +dzl_signal_group_bind_handler (DzlSignalGroup *self, + SignalHandler *handler, + GObject *target) +{ + g_assert (self != NULL); + g_assert (G_IS_OBJECT (target)); + g_assert (handler != NULL); + g_assert (handler->signal_id != 0); + g_assert (handler->closure != NULL); + g_assert (handler->closure->is_invalid == 0); + g_assert (handler->handler_id == 0); + + handler->handler_id = g_signal_connect_closure_by_id (target, + handler->signal_id, + handler->signal_detail, + handler->closure, + handler->connect_after); + + g_assert (handler->handler_id != 0); + + for (guint i = 0; i < self->block_count; i++) + g_signal_handler_block (target, handler->handler_id); +} + +static void +dzl_signal_group_bind (DzlSignalGroup *self, + GObject *target) +{ + g_autoptr(GObject) hold = NULL; + + g_assert (DZL_IS_SIGNAL_GROUP (self)); + g_assert (!target || G_IS_OBJECT (target)); + + if (target == NULL) + return; + + self->has_bound_at_least_once = TRUE; + + hold = g_object_ref (target); + + g_weak_ref_set (&self->target_ref, hold); + g_object_weak_ref (hold, dzl_signal_group__target_weak_notify, self); + + dzl_signal_group_gc_handlers (self); + + for (guint i = 0; i < self->handlers->len; i++) + { + SignalHandler *handler = g_ptr_array_index (self->handlers, i); + + dzl_signal_group_bind_handler (self, handler, hold); + } + + g_signal_emit (self, signals [BIND], 0, hold); +} + +static void +dzl_signal_group_unbind (DzlSignalGroup *self) +{ + g_autoptr(GObject) target = NULL; + + g_return_if_fail (DZL_IS_SIGNAL_GROUP (self)); + + target = g_weak_ref_get (&self->target_ref); + + /* + * Target may be NULL by this point, as we got notified of its destruction. + * However, if we're early enough, we may get a full reference back and can + * cleanly disconnect our connections. + */ + + if (target != NULL) + { + g_weak_ref_set (&self->target_ref, NULL); + + /* + * Let go of our weak reference now that we have a full reference + * for the life of this function. + */ + g_object_weak_unref (target, + dzl_signal_group__target_weak_notify, + self); + } + + dzl_signal_group_gc_handlers (self); + + for (guint i = 0; i < self->handlers->len; i++) + { + SignalHandler *handler; + gulong handler_id; + + handler = g_ptr_array_index (self->handlers, i); + + g_assert (handler != NULL); + g_assert (handler->signal_id != 0); + g_assert (handler->closure != NULL); + + handler_id = handler->handler_id; + handler->handler_id = 0; + + /* + * If @target is NULL, we lost a race to cleanup the weak + * instance and the signal connections have already been + * finalized and therefore nothing to do. + */ + + if (target != NULL && handler_id != 0) + g_signal_handler_disconnect (target, handler_id); + } + + g_signal_emit (self, signals [UNBIND], 0); +} + +static gboolean +dzl_signal_group_check_target_type (DzlSignalGroup *self, + gpointer target) +{ + if ((target != NULL) && + !g_type_is_a (G_OBJECT_TYPE (target), self->target_type)) + { + g_critical ("Failed to set DzlSignalGroup of target type %s " + "using target %p of type %s", + g_type_name (self->target_type), + target, G_OBJECT_TYPE_NAME (target)); + return FALSE; + } + + return TRUE; +} + +/** + * dzl_signal_group_block: + * @self: the #DzlSignalGroup + * + * Blocks all signal handlers managed by @self so they will not + * be called during any signal emissions. Must be unblocked exactly + * the same number of times it has been blocked to become active again. + * + * This blocked state will be kept across changes of the target instance. + * + * See: g_signal_handler_block(). + */ +void +dzl_signal_group_block (DzlSignalGroup *self) +{ + g_autoptr(GObject) target = NULL; + + g_return_if_fail (DZL_IS_SIGNAL_GROUP (self)); + g_return_if_fail (self->block_count != G_MAXSIZE); + + self->block_count++; + + target = g_weak_ref_get (&self->target_ref); + + if (target == NULL) + return; + + for (guint i = 0; i < self->handlers->len; i++) + { + const SignalHandler *handler = g_ptr_array_index (self->handlers, i); + + g_assert (handler != NULL); + g_assert (handler->signal_id != 0); + g_assert (handler->closure != NULL); + g_assert (handler->handler_id != 0); + + g_signal_handler_block (target, handler->handler_id); + } +} + +/** + * dzl_signal_group_unblock: + * @self: the #DzlSignalGroup + * + * Unblocks all signal handlers managed by @self so they will be + * called again during any signal emissions unless it is blocked + * again. Must be unblocked exactly the same number of times it + * has been blocked to become active again. + * + * See: g_signal_handler_unblock(). + */ +void +dzl_signal_group_unblock (DzlSignalGroup *self) +{ + g_autoptr(GObject) target = NULL; + + g_return_if_fail (DZL_IS_SIGNAL_GROUP (self)); + g_return_if_fail (self->block_count != 0); + + self->block_count--; + + target = g_weak_ref_get (&self->target_ref); + + if (target == NULL) + return; + + for (guint i = 0; i < self->handlers->len; i++) + { + const SignalHandler *handler = g_ptr_array_index (self->handlers, i); + + g_assert (handler != NULL); + g_assert (handler->signal_id != 0); + g_assert (handler->closure != NULL); + g_assert (handler->handler_id != 0); + + g_signal_handler_unblock (target, handler->handler_id); + } +} + +/** + * dzl_signal_group_get_target: + * @self: the #DzlSignalGroup + * + * Gets the target instance used when connecting signals. + * + * Returns: (nullable) (transfer none) (type GObject): The target instance. + */ +gpointer +dzl_signal_group_get_target (DzlSignalGroup *self) +{ + g_autoptr(GObject) target = NULL; + + g_return_val_if_fail (DZL_IS_SIGNAL_GROUP (self), NULL); + + target = g_weak_ref_get (&self->target_ref); + + /* + * It is expected that this is called from a thread that owns a reference to + * the target, so we can pass back a borrowed reference. However, to ensure + * that we aren't racing in finalization of @target, we must ensure that the + * ref_count >= 2 (as our get just incremented by one). + */ + + if (target == NULL || target->ref_count < 2) + return NULL; + + /* Unref and pass back a borrowed reference. This looks unsafe, but is safe + * because of our reference check above, so much as the assertion holds that + * the caller obeyed the ownership rules of this class. + */ + return target; +} + +/** + * dzl_signal_group_set_target: + * @self: the #DzlSignalGroup. + * @target: (nullable) (type GObject): The target instance used + * when connecting signals. + * + * Sets the target instance used when connecting signals. Any signal + * that has been registered with dzl_signal_group_connect_object() or + * similar functions will be connected to this object. + * + * If the target instance was previously set, signals will be + * disconnected from that object prior to connecting to @target. + */ +void +dzl_signal_group_set_target (DzlSignalGroup *self, + gpointer target) +{ + g_autoptr(GObject) object = NULL; + + g_return_if_fail (DZL_IS_SIGNAL_GROUP (self)); + + object = g_weak_ref_get (&self->target_ref); + + if (object == (GObject *)target) + return; + + if (!dzl_signal_group_check_target_type (self, target)) + return; + + /* Only emit unbind if we've ever called bind */ + if (self->has_bound_at_least_once) + dzl_signal_group_unbind (self); + + dzl_signal_group_bind (self, target); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TARGET]); +} + +static void +signal_handler_free (gpointer data) +{ + SignalHandler *handler = data; + + if (handler->closure != NULL) + g_closure_invalidate (handler->closure); + + handler->handler_id = 0; + handler->signal_id = 0; + handler->signal_detail = 0; + g_clear_pointer (&handler->closure, g_closure_unref); + g_slice_free (SignalHandler, handler); +} + +static void +dzl_signal_group_constructed (GObject *object) +{ + DzlSignalGroup *self = (DzlSignalGroup *)object; + g_autoptr(GObject) target = g_weak_ref_get (&self->target_ref); + + if (!dzl_signal_group_check_target_type (self, target)) + dzl_signal_group_set_target (self, NULL); + + G_OBJECT_CLASS (dzl_signal_group_parent_class)->constructed (object); +} + +static void +dzl_signal_group_dispose (GObject *object) +{ + DzlSignalGroup *self = (DzlSignalGroup *)object; + + dzl_signal_group_gc_handlers (self); + + if (self->has_bound_at_least_once) + dzl_signal_group_unbind (self); + + g_clear_pointer (&self->handlers, g_ptr_array_unref); + + G_OBJECT_CLASS (dzl_signal_group_parent_class)->dispose (object); +} + +static void +dzl_signal_group_finalize (GObject *object) +{ + DzlSignalGroup *self = (DzlSignalGroup *)object; + + g_weak_ref_clear (&self->target_ref); + + G_OBJECT_CLASS (dzl_signal_group_parent_class)->finalize (object); +} + +static void +dzl_signal_group_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSignalGroup *self = DZL_SIGNAL_GROUP (object); + + switch (prop_id) + { + case PROP_TARGET: + g_value_take_object (value, g_weak_ref_get (&self->target_ref)); + break; + + case PROP_TARGET_TYPE: + g_value_set_gtype (value, self->target_type); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_signal_group_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSignalGroup *self = DZL_SIGNAL_GROUP (object); + + switch (prop_id) + { + case PROP_TARGET: + dzl_signal_group_set_target (self, g_value_get_object (value)); + break; + + case PROP_TARGET_TYPE: + dzl_signal_group_set_target_type (self, g_value_get_gtype (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_signal_group_class_init (DzlSignalGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = dzl_signal_group_constructed; + object_class->dispose = dzl_signal_group_dispose; + object_class->finalize = dzl_signal_group_finalize; + object_class->get_property = dzl_signal_group_get_property; + object_class->set_property = dzl_signal_group_set_property; + + /** + * DzlSignalGroup:target + * + * The target instance used when connecting signals. + */ + properties [PROP_TARGET] = + g_param_spec_object ("target", + "Target", + "The target instance used when connecting signals.", + G_TYPE_OBJECT, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + /** + * DzlSignalGroup:target-type + * + * The GType of the target property. + */ + properties [PROP_TARGET_TYPE] = + g_param_spec_gtype ("target-type", + "Target Type", + "The GType of the target property.", + G_TYPE_OBJECT, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + /** + * DzlSignalGroup::bind: + * @self: the #DzlSignalGroup + * @instance: a #GObject + * + * This signal is emitted when the target instance of @self + * is set to a new #GObject. + * + * This signal will only be emitted if the target of @self is non-%NULL. + */ + signals [BIND] = + g_signal_new ("bind", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_OBJECT); + + /** + * DzlSignalGroup::unbind: + * @self: a #DzlSignalGroup + * + * This signal is emitted when the target instance of @self + * is set to a new #GObject. + * + * This signal will only be emitted if the previous target + * of @self is non-%NULL. + */ + signals [UNBIND] = + g_signal_new ("unbind", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); +} + +static void +dzl_signal_group_init (DzlSignalGroup *self) +{ + self->handlers = g_ptr_array_new_with_free_func (signal_handler_free); + self->target_type = G_TYPE_OBJECT; +} + +/** + * dzl_signal_group_new: + * @target_type: the #GType of the target instance. + * + * Creates a new #DzlSignalGroup for target instances of @target_type. + * + * Returns: a new #DzlSignalGroup + */ +DzlSignalGroup * +dzl_signal_group_new (GType target_type) +{ + g_return_val_if_fail (g_type_is_a (target_type, G_TYPE_OBJECT), NULL); + + return g_object_new (DZL_TYPE_SIGNAL_GROUP, + "target-type", target_type, + NULL); +} + +static void +dzl_signal_group_connect_full (DzlSignalGroup *self, + const gchar *detailed_signal, + GCallback callback, + gpointer data, + GClosureNotify notify, + GConnectFlags flags, + gboolean is_object) +{ + g_autoptr(GObject) target = NULL; + SignalHandler *handler; + GClosure *closure; + guint signal_id; + GQuark signal_detail; + + g_return_if_fail (DZL_IS_SIGNAL_GROUP (self)); + g_return_if_fail (detailed_signal != NULL); + g_return_if_fail (g_signal_parse_name (detailed_signal, self->target_type, + &signal_id, &signal_detail, TRUE) != 0); + g_return_if_fail (callback != NULL); + g_return_if_fail (!is_object || G_IS_OBJECT (data)); + + if ((flags & G_CONNECT_SWAPPED) != 0) + closure = g_cclosure_new_swap (callback, data, notify); + else + closure = g_cclosure_new (callback, data, notify); + + handler = g_slice_new0 (SignalHandler); + handler->group = self; + handler->signal_id = signal_id; + handler->signal_detail = signal_detail; + handler->closure = g_closure_ref (closure); + handler->connect_after = ((flags & G_CONNECT_AFTER) != 0); + + g_closure_sink (closure); + + if (is_object) + { + /* Set closure->is_invalid when data is disposed. We only track this to avoid + * reconnecting in the future. However, we do a round of cleanup when ever we + * connect a new object or the target changes to GC the old handlers. + */ + g_object_watch_closure (data, closure); + } + + g_ptr_array_add (self->handlers, handler); + + target = g_weak_ref_get (&self->target_ref); + if (target != NULL) + dzl_signal_group_bind_handler (self, handler, target); + + /* Lazily remove any old handlers on connect */ + dzl_signal_group_gc_handlers (self); +} + +/** + * dzl_signal_group_connect_object: (skip) + * @self: a #DzlSignalGroup + * @detailed_signal: a string of the form "signal-name::detail" + * @c_handler: (scope notified): the #GCallback to connect + * @object: the #GObject to pass as data to @callback calls + * + * Connects @callback to the signal @detailed_signal + * on the target object of @self. + * + * Ensures that the @object stays alive during the call to @callback + * by temporarily adding a reference count. When the @object is destroyed + * the signal handler will automatically be removed. + * + * See: g_signal_connect_object(). + */ +void +dzl_signal_group_connect_object (DzlSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer object, + GConnectFlags flags) +{ + g_return_if_fail (G_IS_OBJECT (object)); + + dzl_signal_group_connect_full (self, detailed_signal, c_handler, object, NULL, + flags, TRUE); +} + +/** + * dzl_signal_group_connect_data: + * @self: a #DzlSignalGroup + * @detailed_signal: a string of the form "signal-name::detail" + * @c_handler: (scope notified) (closure data) (destroy notify): the #GCallback to connect + * @data: the data to pass to @callback calls + * @notify: function to be called when disposing of @self + * @flags: the flags used to create the signal connection + * + * Connects @callback to the signal @detailed_signal + * on the target instance of @self. + * + * See: g_signal_connect_data(). + */ +void +dzl_signal_group_connect_data (DzlSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data, + GClosureNotify notify, + GConnectFlags flags) +{ + dzl_signal_group_connect_full (self, detailed_signal, c_handler, data, notify, + flags, FALSE); +} + +/** + * dzl_signal_group_connect: (skip) + * @self: a #DzlSignalGroup + * @detailed_signal: a string of the form "signal-name::detail" + * @c_handler: (scope notified): the #GCallback to connect + * @data: the data to pass to @callback calls + * + * Connects @callback to the signal @detailed_signal + * on the target instance of @self. + * + * See: g_signal_connect(). + */ +void +dzl_signal_group_connect (DzlSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data) +{ + dzl_signal_group_connect_full (self, detailed_signal, c_handler, data, NULL, + 0, FALSE); +} + +/** + * dzl_signal_group_connect_after: (skip) + * @self: a #DzlSignalGroup + * @detailed_signal: a string of the form "signal-name::detail" + * @c_handler: (scope notified): the #GCallback to connect + * @data: the data to pass to @callback calls + * + * Connects @callback to the signal @detailed_signal + * on the target instance of @self. + * + * The @callback will be called after the default handler of the signal. + * + * See: g_signal_connect_after(). + */ +void +dzl_signal_group_connect_after (DzlSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data) +{ + dzl_signal_group_connect_full (self, detailed_signal, c_handler, + data, NULL, G_CONNECT_AFTER, FALSE); +} + +/** + * dzl_signal_group_connect_swapped: + * @self: a #DzlSignalGroup + * @detailed_signal: a string of the form "signal-name::detail" + * @c_handler: (scope async): the #GCallback to connect + * @data: the data to pass to @callback calls + * + * Connects @callback to the signal @detailed_signal + * on the target instance of @self. + * + * The instance on which the signal is emitted and @data + * will be swapped when calling @callback. + * + * See: g_signal_connect_swapped(). + */ +void +dzl_signal_group_connect_swapped (DzlSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data) +{ + dzl_signal_group_connect_full (self, detailed_signal, c_handler, data, NULL, + G_CONNECT_SWAPPED, FALSE); +} diff --git a/src/bindings/dzl-signal-group.h b/src/bindings/dzl-signal-group.h new file mode 100644 index 0000000..5fe646b --- /dev/null +++ b/src/bindings/dzl-signal-group.h @@ -0,0 +1,79 @@ +/* dzl-signal-group.h + * + * Copyright (C) 2015 Christian Hergert + * Copyright (C) 2015 Garrett Regier + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_SIGNAL_GROUP_H +#define DZL_SIGNAL_GROUP_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SIGNAL_GROUP (dzl_signal_group_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlSignalGroup, dzl_signal_group, DZL, SIGNAL_GROUP, GObject) + +DZL_AVAILABLE_IN_ALL +DzlSignalGroup *dzl_signal_group_new (GType target_type); + +DZL_AVAILABLE_IN_ALL +void dzl_signal_group_set_target (DzlSignalGroup *self, + gpointer target); +DZL_AVAILABLE_IN_ALL +gpointer dzl_signal_group_get_target (DzlSignalGroup *self); + +DZL_AVAILABLE_IN_ALL +void dzl_signal_group_block (DzlSignalGroup *self); +DZL_AVAILABLE_IN_ALL +void dzl_signal_group_unblock (DzlSignalGroup *self); + +DZL_AVAILABLE_IN_ALL +void dzl_signal_group_connect_object (DzlSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer object, + GConnectFlags flags); +DZL_AVAILABLE_IN_ALL +void dzl_signal_group_connect_data (DzlSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data, + GClosureNotify notify, + GConnectFlags flags); +DZL_AVAILABLE_IN_ALL +void dzl_signal_group_connect (DzlSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data); +DZL_AVAILABLE_IN_ALL +void dzl_signal_group_connect_after (DzlSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data); +DZL_AVAILABLE_IN_ALL +void dzl_signal_group_connect_swapped (DzlSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data); + +G_END_DECLS + +#endif /* DZL_SIGNAL_GROUP_H */ diff --git a/src/bindings/meson.build b/src/bindings/meson.build new file mode 100644 index 0000000..27c9732 --- /dev/null +++ b/src/bindings/meson.build @@ -0,0 +1,14 @@ +bindings_headers = [ + 'dzl-binding-group.h', + 'dzl-signal-group.h', +] + +bindings_sources = [ + 'dzl-binding-group.c', + 'dzl-signal-group.c', +] + +libdazzle_public_headers += files(bindings_headers) +libdazzle_public_sources += files(bindings_sources) + +install_headers(bindings_headers, subdir: join_paths(libdazzle_header_subdir, 'bindings')) diff --git a/src/cache/dzl-task-cache.c b/src/cache/dzl-task-cache.c new file mode 100644 index 0000000..a04ce36 --- /dev/null +++ b/src/cache/dzl-task-cache.c @@ -0,0 +1,1042 @@ +/* dzl-task-cache.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-task-cache" + +#include "config.h" + +#include + +#include "cache/dzl-task-cache.h" +#include "util/dzl-heap.h" + +typedef struct +{ + DzlTaskCache *self; + gpointer key; + gpointer value; + gint64 evict_at; +} CacheItem; + +typedef struct +{ + DzlTaskCache *self; + GCancellable *cancellable; + gpointer key; + gulong cancelled_id; +} CancelledData; + +typedef struct +{ + GSource source; + DzlHeap *heap; +} EvictSource; + +struct _DzlTaskCache +{ + GObject parent_instance; + + GHashFunc key_hash_func; + GEqualFunc key_equal_func; + GBoxedCopyFunc key_copy_func; + GBoxedFreeFunc key_destroy_func; + GBoxedCopyFunc value_copy_func; + GBoxedFreeFunc value_destroy_func; + + DzlTaskCacheCallback populate_callback; + gpointer populate_callback_data; + GDestroyNotify populate_callback_data_destroy; + + GHashTable *cache; + GHashTable *in_flight; + GHashTable *queued; + + gchar *name; + + DzlHeap *evict_heap; + GSource *evict_source; + guint evict_source_id; + + gint64 time_to_live_usec; +}; + +G_DEFINE_TYPE (DzlTaskCache, dzl_task_cache, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_KEY_COPY_FUNC, + PROP_KEY_DESTROY_FUNC, + PROP_KEY_EQUAL_FUNC, + PROP_KEY_HASH_FUNC, + PROP_POPULATE_CALLBACK, + PROP_POPULATE_CALLBACK_DATA, + PROP_POPULATE_CALLBACK_DATA_DESTROY, + PROP_TIME_TO_LIVE, + PROP_VALUE_COPY_FUNC, + PROP_VALUE_DESTROY_FUNC, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +static gboolean +evict_source_check (GSource *source) +{ + EvictSource *ev = (EvictSource *)source; + + g_assert (ev != NULL); + g_assert (ev->heap != NULL); + + if (ev->heap->len > 0) + { + CacheItem *item; + gint64 now; + + now = g_source_get_time (source); + item = dzl_heap_peek (ev->heap, gpointer); + + return (item->evict_at <= now); + } + + return FALSE; +} + +static void +evict_source_rearm (GSource *source) +{ + EvictSource *evict_source = (EvictSource *)source; + gint64 ready_time = -1; + + g_assert (source != NULL); + g_assert (evict_source != NULL); + + if (evict_source->heap->len > 0) + { + CacheItem *item; + + item = dzl_heap_peek (evict_source->heap, gpointer); + ready_time = item->evict_at; + } + + g_source_set_ready_time (source, ready_time); +} + +static gboolean +evict_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + gboolean ret = G_SOURCE_CONTINUE; + + if (callback != NULL) + ret = callback (user_data); + + evict_source_rearm (source); + + return ret; +} + +static void +evict_source_finalize (GSource *source) +{ + EvictSource *ev = (EvictSource *)source; + + g_clear_pointer (&ev->heap, dzl_heap_unref); +} + +static GSourceFuncs evict_source_funcs = { + NULL, + evict_source_check, + evict_source_dispatch, + evict_source_finalize, +}; + +static void +cache_item_free (gpointer data) +{ + CacheItem *item = data; + + g_clear_pointer (&item->key, item->self->key_destroy_func); + g_clear_pointer (&item->value, item->self->value_destroy_func); + item->self = NULL; + item->evict_at = 0; + + g_slice_free (CacheItem, item); +} + +static gint +cache_item_compare_evict_at (gconstpointer a, + gconstpointer b) +{ + const CacheItem **ci1 = (const CacheItem **)a; + const CacheItem **ci2 = (const CacheItem **)b; + gint64 ret; + + /* + * While unlikely, but since we are working with 64-bit monotonic clock and + * 32-bit return values, we can't do the normal (a - b) trick. We need to + * ensure we are within the 32-bit boundary. + */ + + ret = (*ci2)->evict_at - (*ci1)->evict_at; + + if (ret < 0) + return -1; + else if (ret > 0) + return 1; + else + return 0; +} + +static CacheItem * +cache_item_new (DzlTaskCache *self, + gconstpointer key, + gconstpointer value) +{ + CacheItem *ret; + + g_assert (DZL_IS_TASK_CACHE (self)); + + ret = g_slice_new0 (CacheItem); + ret->self = self; + ret->key = self->key_copy_func ((gpointer)key); + ret->value = self->value_copy_func ((gpointer)value); + if (self->time_to_live_usec > 0) + ret->evict_at = g_get_monotonic_time () + self->time_to_live_usec; + + return ret; +} + +static void +cancelled_data_free (gpointer data) +{ + CancelledData *cancelled = data; + + g_clear_pointer (&cancelled->key, cancelled->self->key_destroy_func); + + g_cancellable_disconnect (cancelled->cancellable, cancelled->cancelled_id); + cancelled->cancelled_id = 0; + g_clear_object (&cancelled->cancellable); + + cancelled->self = NULL; + + g_slice_free (CancelledData, cancelled); +} + +static CancelledData * +cancelled_data_new (DzlTaskCache *self, + GCancellable *cancellable, + gconstpointer key, + gulong cancelled_id) +{ + CancelledData *ret; + + ret = g_slice_new0 (CancelledData); + ret->self = self; + ret->cancellable = (cancellable != NULL) ? g_object_ref (cancellable) : NULL; + ret->key = self->key_copy_func ((gpointer)key); + ret->cancelled_id = cancelled_id; + + return ret; +} + +static gpointer +dzl_task_cache_dummy_copy_func (gpointer boxed) +{ + return boxed; +} + +static void +dzl_task_cache_dummy_destroy_func (gpointer boxed) +{ +} + +static gboolean +dzl_task_cache_evict_full (DzlTaskCache *self, + gconstpointer key, + gboolean check_heap) +{ + CacheItem *item; + + g_return_val_if_fail (DZL_IS_TASK_CACHE (self), FALSE); + + if ((item = g_hash_table_lookup (self->cache, key))) + { + if (check_heap) + { + gsize i; + + for (i = 0; i < self->evict_heap->len; i++) + { + if (item == dzl_heap_index (self->evict_heap, gpointer, i)) + { + dzl_heap_extract_index (self->evict_heap, i, NULL); + break; + } + } + } + + g_hash_table_remove (self->cache, key); + + g_debug ("Evicted 1 item from %s", self->name ?: "unnamed cache"); + + if (self->evict_source != NULL) + evict_source_rearm (self->evict_source); + + return TRUE; + } + + return FALSE; +} + +gboolean +dzl_task_cache_evict (DzlTaskCache *self, + gconstpointer key) +{ + return dzl_task_cache_evict_full (self, key, TRUE); +} + +void +dzl_task_cache_evict_all (DzlTaskCache *self) +{ + g_return_if_fail (DZL_IS_TASK_CACHE (self)); + + while (self->evict_heap->len > 0) + { + CacheItem *item; + + /* The cache item is owned by the hashtable, so safe to "leak" here */ + dzl_heap_extract_index (self->evict_heap, self->evict_heap->len - 1, &item); + } + + g_hash_table_remove_all (self->cache); + + if (self->evict_source != NULL) + evict_source_rearm (self->evict_source); +} + +/** + * dzl_task_cache_peek: + * @self: An #DzlTaskCache + * @key: The key for the cache + * + * Peeks to see @key is contained in the cache and returns the + * matching #GObject if it does. + * + * The reference count of the resulting #GObject is not incremented. + * For that reason, it is important to remember that this function + * may only be called from the main thread. + * + * Returns: (type GObject.Object) (nullable) (transfer none): A #GObject or + * %NULL if the key was not found in the cache. + */ +gpointer +dzl_task_cache_peek (DzlTaskCache *self, + gconstpointer key) +{ + CacheItem *item; + + g_return_val_if_fail (DZL_IS_TASK_CACHE (self), NULL); + + if (NULL != (item = g_hash_table_lookup (self->cache, key))) + return item->value; + + return NULL; +} + +static void +dzl_task_cache_propagate_error (DzlTaskCache *self, + gconstpointer key, + const GError *error) +{ + GPtrArray *queued; + + g_assert (DZL_IS_TASK_CACHE (self)); + g_assert (error != NULL); + + if (NULL != (queued = g_hash_table_lookup (self->queued, key))) + { + /* we can't use steal because we want the key freed */ + g_ptr_array_ref (queued); + g_hash_table_remove (self->queued, key); + + for (guint i = 0; i < queued->len; i++) + { + GTask *task; + + task = g_ptr_array_index (queued, i); + g_task_return_error (task, g_error_copy (error)); + } + + g_ptr_array_unref (queued); + } +} + +static void +dzl_task_cache_populate (DzlTaskCache *self, + gconstpointer key, + gpointer value) +{ + CacheItem *item; + + g_assert (DZL_IS_TASK_CACHE (self)); + + item = cache_item_new (self, key, value); + + if (g_hash_table_contains (self->cache, key)) + dzl_task_cache_evict (self, key); + g_hash_table_insert (self->cache, item->key, item); + dzl_heap_insert_val (self->evict_heap, item); + + if (self->evict_source != NULL) + evict_source_rearm (self->evict_source); +} + +static void +dzl_task_cache_propagate_pointer (DzlTaskCache *self, + gconstpointer key, + gpointer value) +{ + GPtrArray *queued = NULL; + + g_assert (DZL_IS_TASK_CACHE (self)); + + if (NULL != (queued = g_hash_table_lookup (self->queued, key))) + { + g_ptr_array_ref (queued); + g_hash_table_remove (self->queued, key); + + for (guint i = 0; i < queued->len; i++) + { + GTask *task = g_ptr_array_index (queued, i); + + g_task_return_pointer (task, + self->value_copy_func (value), + self->value_destroy_func); + } + + g_ptr_array_unref (queued); + } +} + +static gboolean +dzl_task_cache_cancel_in_idle (gpointer user_data) +{ + DzlTaskCache *self; + CancelledData *data; + GCancellable *cancellable; + GPtrArray *queued; + GTask *task = user_data; + gboolean cancelled = FALSE; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + cancellable = g_task_get_cancellable (task); + data = g_task_get_task_data (task); + + g_assert (DZL_IS_TASK_CACHE (self)); + g_assert (G_IS_CANCELLABLE (cancellable)); + g_assert (data != NULL); + g_assert (data->self == self); + g_assert (data->cancellable == cancellable); + + if ((queued = g_hash_table_lookup (self->queued, data->key))) + { + for (guint i = 0; i < queued->len; i++) + { + GCancellable *queued_cancellable; + GTask *queued_task; + + queued_task = g_ptr_array_index (queued, i); + queued_cancellable = g_task_get_cancellable (queued_task); + + if (queued_task == task && queued_cancellable == cancellable) + { + cancelled = g_task_return_error_if_cancelled (task); + g_ptr_array_remove_index_fast (queued, i); + break; + } + } + + if (queued->len == 0) + { + GTask *fetch_task; + + if ((fetch_task = g_hash_table_lookup (self->in_flight, data->key))) + { + GCancellable *fetch_cancellable; + + fetch_cancellable = g_task_get_cancellable (fetch_task); + g_cancellable_cancel (fetch_cancellable); + } + } + + g_return_val_if_fail (cancelled, G_SOURCE_REMOVE); + } + + return G_SOURCE_REMOVE; +} + +static void +dzl_task_cache_cancelled_cb (GCancellable *cancellable, + gpointer user_data) +{ + DzlTaskCache *self; + CancelledData *data; + GMainContext *context; + g_autoptr(GSource) source = NULL; + GTask *task = user_data; + + g_assert (G_IS_CANCELLABLE (cancellable)); + g_assert (G_IS_TASK (task)); + + self = g_task_get_source_object (task); + data = g_task_get_task_data (task); + + g_assert (DZL_IS_TASK_CACHE (self)); + g_assert (data != NULL); + g_assert (data->self == self); + g_assert (data->cancellable == cancellable); + + source = g_idle_source_new (); + g_source_set_callback (source, dzl_task_cache_cancel_in_idle, g_object_ref (task), g_object_unref); + g_source_set_name (source, "[dzl] dzl_task_cache_cancel_in_idle"); + + context = g_main_context_get_thread_default (); + g_source_attach (source, context); +} + +static void +dzl_task_cache_fetch_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + DzlTaskCache *self = (DzlTaskCache *)object; + GTask *task = (GTask *)result; + GError *error = NULL; + gpointer key = user_data; + gpointer ret; + + g_assert (DZL_IS_TASK_CACHE (self)); + g_assert (G_IS_TASK (task)); + + g_hash_table_remove (self->in_flight, key); + + ret = g_task_propagate_pointer (task, &error); + + if (ret != NULL) + { + dzl_task_cache_populate (self, key, ret); + dzl_task_cache_propagate_pointer (self, key, ret); + self->value_destroy_func (ret); + } + else + { + dzl_task_cache_propagate_error (self, key, error); + g_clear_error (&error); + } + + self->key_destroy_func (key); + g_object_unref (task); +} + +void +dzl_task_cache_get_async (DzlTaskCache *self, + gconstpointer key, + gboolean force_update, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) fetch_task = NULL; + g_autoptr(GTask) task = NULL; + CancelledData *data; + GPtrArray *queued; + gpointer ret; + gulong cancelled_id = 0; + + g_return_if_fail (DZL_IS_TASK_CACHE (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_return_on_cancel (task, FALSE); + + /* + * If we have the answer, return it now. + */ + if (!force_update && (ret = dzl_task_cache_peek (self, key))) + { + g_task_return_pointer (task, + self->value_copy_func (ret), + self->value_destroy_func); + return; + } + + /* + * Always queue the request. If we need to dispatch the worker to + * fetch the result, that will happen with another task. + */ + if (!(queued = g_hash_table_lookup (self->queued, key))) + { + queued = g_ptr_array_new_with_free_func (g_object_unref); + g_hash_table_insert (self->queued, + self->key_copy_func ((gpointer)key), + queued); + } + + g_ptr_array_add (queued, g_object_ref (task)); + + /* + * The in_flight hashtable will have a bit set if we have queued + * an operation for this key. + */ + if (!g_hash_table_contains (self->in_flight, key)) + { + g_autoptr(GCancellable) fetch_cancellable = NULL; + + fetch_cancellable = g_cancellable_new (); + fetch_task = g_task_new (self, + fetch_cancellable, + dzl_task_cache_fetch_cb, + self->key_copy_func ((gpointer)key)); + g_hash_table_insert (self->in_flight, + self->key_copy_func ((gpointer)key), + g_object_ref (fetch_task)); + } + + if (cancellable != NULL) + { + cancelled_id = g_cancellable_connect (cancellable, + G_CALLBACK (dzl_task_cache_cancelled_cb), + task, + NULL); + } + + data = cancelled_data_new (self, cancellable, key, cancelled_id); + g_task_set_task_data (task, data, cancelled_data_free); + + if (fetch_task != NULL) + { + self->populate_callback (self, + key, + g_object_ref (fetch_task), + self->populate_callback_data); + } +} + +/** + * dzl_task_cache_get_finish: + * + * Finish a call to dzl_task_cache_get_async(). + * + * Returns: (transfer full): The result from the cache. + */ +gpointer +dzl_task_cache_get_finish (DzlTaskCache *self, + GAsyncResult *result, + GError **error) +{ + GTask *task = (GTask *)result; + + g_return_val_if_fail (DZL_IS_TASK_CACHE (self), NULL); + g_return_val_if_fail (G_IS_TASK (result), NULL); + g_return_val_if_fail (G_IS_TASK (task), NULL); + + return g_task_propagate_pointer (task, error); +} + +static gboolean +dzl_task_cache_do_eviction (gpointer user_data) +{ + DzlTaskCache *self = user_data; + gint64 now = g_get_monotonic_time (); + + while (self->evict_heap->len > 0) + { + CacheItem *item; + + item = dzl_heap_peek (self->evict_heap, gpointer); + + if (item->evict_at <= now) + { + dzl_heap_extract (self->evict_heap, NULL); + dzl_task_cache_evict_full (self, item->key, FALSE); + continue; + } + + break; + } + + return G_SOURCE_CONTINUE; +} + +static void +dzl_task_cache_install_evict_source (DzlTaskCache *self) +{ + GMainContext *main_context; + EvictSource *evict_source; + GSource *source; + + main_context = g_main_context_get_thread_default (); + + source = g_source_new (&evict_source_funcs, sizeof (EvictSource)); + g_source_set_callback (source, dzl_task_cache_do_eviction, self, NULL); + g_source_set_name (source, "DzlTaskCache Eviction"); + g_source_set_priority (source, G_PRIORITY_LOW); + g_source_set_ready_time (source, -1); + + evict_source = (EvictSource *)source; + evict_source->heap = dzl_heap_ref (self->evict_heap); + + self->evict_source = source; + self->evict_source_id = g_source_attach (source, main_context); +} + +static void +dzl_task_cache_constructed (GObject *object) +{ + DzlTaskCache *self = (DzlTaskCache *)object; + + G_OBJECT_CLASS (dzl_task_cache_parent_class)->constructed (object); + + if ((self->key_equal_func == NULL) || + (self->key_hash_func == NULL) || + (self->value_copy_func == NULL) || + (self->value_destroy_func == NULL) || + (self->populate_callback == NULL)) + { + g_error ("DzlTaskCache was configured improperly."); + return; + } + + if (self->key_copy_func == NULL) + self->key_copy_func = dzl_task_cache_dummy_copy_func; + + if (self->key_destroy_func == NULL) + self->key_destroy_func = dzl_task_cache_dummy_destroy_func; + + /* + * This is where the cached result objects live. + */ + self->cache = g_hash_table_new_full (self->key_hash_func, + self->key_equal_func, + NULL, + cache_item_free); + + /* + * This is where we store a bit to know if we have an inflight + * request for this cache key. + */ + self->in_flight = g_hash_table_new_full (self->key_hash_func, + self->key_equal_func, + self->key_destroy_func, + g_object_unref); + + /* + * This is where tasks queue waiting for an in_flight callback. + */ + self->queued = g_hash_table_new_full (self->key_hash_func, + self->key_equal_func, + self->key_destroy_func, + (GDestroyNotify)g_ptr_array_unref); + + /* + * Register our eviction source if we have a time_to_live. + */ + if (self->time_to_live_usec > 0) + dzl_task_cache_install_evict_source (self); +} + +static void +dzl_task_cache_dispose (GObject *object) +{ + DzlTaskCache *self = (DzlTaskCache *)object; + + if (self->evict_source_id != 0) + { + g_source_remove (self->evict_source_id); + self->evict_source_id = 0; + self->evict_source = NULL; + } + + g_clear_pointer (&self->evict_heap, dzl_heap_unref); + + if (self->cache != NULL) + { + gint64 count; + + count = g_hash_table_size (self->cache); + g_clear_pointer (&self->cache, g_hash_table_unref); + + g_debug ("Evicted cache of %"G_GINT64_FORMAT" items from %s", + count, self->name ?: "unnamed cache"); + } + + if (self->queued != NULL) + g_clear_pointer (&self->queued, g_hash_table_unref); + + if (self->in_flight != NULL) + g_clear_pointer (&self->in_flight, (GDestroyNotify)g_hash_table_unref); + + if (self->populate_callback_data) + { + if (self->populate_callback_data_destroy) + self->populate_callback_data_destroy (self->populate_callback_data); + } + + G_OBJECT_CLASS (dzl_task_cache_parent_class)->dispose (object); +} + +static void +dzl_task_cache_finalize (GObject *object) +{ + DzlTaskCache *self = (DzlTaskCache *)object; + + g_clear_pointer (&self->name, g_free); + + G_OBJECT_CLASS (dzl_task_cache_parent_class)->finalize (object); +} + +static void +dzl_task_cache_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlTaskCache *self = DZL_TASK_CACHE(object); + + switch (prop_id) + { + case PROP_KEY_COPY_FUNC: + self->key_copy_func = g_value_get_pointer (value); + break; + + case PROP_KEY_DESTROY_FUNC: + self->key_destroy_func = g_value_get_pointer (value); + break; + + case PROP_KEY_EQUAL_FUNC: + self->key_equal_func = g_value_get_pointer (value); + break; + + case PROP_KEY_HASH_FUNC: + self->key_hash_func = g_value_get_pointer (value); + break; + + case PROP_POPULATE_CALLBACK: + self->populate_callback = g_value_get_pointer (value); + break; + + case PROP_POPULATE_CALLBACK_DATA: + self->populate_callback_data = g_value_get_pointer (value); + break; + + case PROP_POPULATE_CALLBACK_DATA_DESTROY: + self->populate_callback_data_destroy = g_value_get_pointer (value); + break; + + case PROP_TIME_TO_LIVE: + self->time_to_live_usec = (g_value_get_int64 (value) * 1000L); + break; + + case PROP_VALUE_COPY_FUNC: + self->value_copy_func = g_value_get_pointer (value); + break; + + case PROP_VALUE_DESTROY_FUNC: + self->value_destroy_func = g_value_get_pointer (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + } +} + +static void +dzl_task_cache_class_init (DzlTaskCacheClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = dzl_task_cache_constructed; + object_class->dispose = dzl_task_cache_dispose; + object_class->finalize = dzl_task_cache_finalize; + object_class->set_property = dzl_task_cache_set_property; + + properties [PROP_KEY_HASH_FUNC] = + g_param_spec_pointer ("key-hash-func", + "Key Hash Func", + "Key Hash Func", + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_KEY_EQUAL_FUNC] = + g_param_spec_pointer ("key-equal-func", + "Key Equal Func", + "Key Equal Func", + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_KEY_COPY_FUNC] = + g_param_spec_pointer ("key-copy-func", + "Key Copy Func", + "Key Copy Func", + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_KEY_DESTROY_FUNC] = + g_param_spec_pointer ("key-destroy-func", + "Key Destroy Func", + "Key Destroy Func", + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_POPULATE_CALLBACK] = + g_param_spec_pointer ("populate-callback", + "Populate Callback", + "Populate Callback", + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_POPULATE_CALLBACK_DATA] = + g_param_spec_pointer ("populate-callback-data", + "Populate Callback Data", + "Populate Callback Data", + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_POPULATE_CALLBACK_DATA_DESTROY] = + g_param_spec_pointer ("populate-callback-data-destroy", + "Populate Callback Data Destroy", + "Populate Callback Data Destroy", + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + /** + * DzlTaskCache:time-to-live: + * + * This is the number of milliseconds before an item should be evicted + * from the cache. + * + * A value of zero indicates no eviction. + */ + properties [PROP_TIME_TO_LIVE] = + g_param_spec_int64 ("time-to-live", + "Time to Live", + "The time to live in milliseconds.", + 0, + G_MAXINT64, + 30 * 1000, + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_VALUE_COPY_FUNC] = + g_param_spec_pointer ("value-copy-func", + "Value Copy Func", + "Value Copy Func", + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_VALUE_DESTROY_FUNC] = + g_param_spec_pointer ("value-destroy-func", + "Value Destroy Func", + "Value Destroy Func", + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +void +dzl_task_cache_init (DzlTaskCache *self) +{ + self->evict_heap = dzl_heap_new (sizeof (gpointer), cache_item_compare_evict_at); +} + +/** + * dzl_task_cache_new: (skip) + */ +DzlTaskCache * +dzl_task_cache_new (GHashFunc key_hash_func, + GEqualFunc key_equal_func, + GBoxedCopyFunc key_copy_func, + GBoxedFreeFunc key_destroy_func, + GBoxedCopyFunc value_copy_func, + GBoxedFreeFunc value_destroy_func, + gint64 time_to_live, + DzlTaskCacheCallback populate_callback, + gpointer populate_callback_data, + GDestroyNotify populate_callback_data_destroy) +{ + g_return_val_if_fail (key_hash_func, NULL); + g_return_val_if_fail (key_equal_func, NULL); + g_return_val_if_fail (populate_callback, NULL); + + return g_object_new (DZL_TYPE_TASK_CACHE, + "key-hash-func", key_hash_func, + "key-equal-func", key_equal_func, + "key-copy-func", key_copy_func, + "key-destroy-func", key_destroy_func, + "populate-callback", populate_callback, + "populate-callback-data", populate_callback_data, + "populate-callback-data-destroy", populate_callback_data_destroy, + "time-to-live", time_to_live, + "value-copy-func", value_copy_func, + "value-destroy-func", value_destroy_func, + NULL); +} + +/** + * dzl_task_cache_get_values: (skip) + * + * Gets all the values in the cache. + * + * The caller owns the resulting GPtrArray, which itself owns a reference to the children. + * + * Returns: (transfer container): The values. + */ +GPtrArray * +dzl_task_cache_get_values (DzlTaskCache *self) +{ + GPtrArray *ar; + GHashTableIter iter; + gpointer value; + + g_return_val_if_fail (DZL_IS_TASK_CACHE (self), NULL); + + ar = g_ptr_array_new_with_free_func (self->value_destroy_func); + + g_hash_table_iter_init (&iter, self->cache); + + while (g_hash_table_iter_next (&iter, NULL, &value)) + { + CacheItem *item = value; + + g_ptr_array_add (ar, self->value_copy_func (item->value)); + } + + return ar; +} + +void +dzl_task_cache_set_name (DzlTaskCache *self, + const gchar *name) +{ + g_return_if_fail (DZL_IS_TASK_CACHE (self)); + + g_free (self->name); + self->name = g_strdup (name); + + if (name && self->evict_source) + { + g_autofree gchar *full_name = NULL; + + full_name = g_strdup_printf ("[dzl_task_cache] %s", name); + g_source_set_name (self->evict_source, full_name); + } +} diff --git a/src/cache/dzl-task-cache.h b/src/cache/dzl-task-cache.h new file mode 100644 index 0000000..76b6b59 --- /dev/null +++ b/src/cache/dzl-task-cache.h @@ -0,0 +1,93 @@ +/* dzl-task-cache.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_TASK_CACHE_H +#define DZL_TASK_CACHE_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_TASK_CACHE (dzl_task_cache_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlTaskCache, dzl_task_cache, DZL, TASK_CACHE, GObject) + +/** + * DzlTaskCacheCallback: + * @self: An #DzlTaskCache. + * @key: the key to fetch + * @task: the task to be completed + * @user_data: user_data registered at initialization. + * + * #DzlTaskCacheCallback is the prototype for a function to be executed to + * populate an item in the cache. + * + * This function will be executed when a fault (cache miss) occurs from + * a caller requesting an item from the cache. + * + * The callee may complete the operation asynchronously, but MUST return + * either a GObject using g_task_return_pointer() or a #GError using + * g_task_return_error() or g_task_return_new_error(). + */ +typedef void (*DzlTaskCacheCallback) (DzlTaskCache *self, + gconstpointer key, + GTask *task, + gpointer user_data); + +DZL_AVAILABLE_IN_ALL +DzlTaskCache *dzl_task_cache_new (GHashFunc key_hash_func, + GEqualFunc key_equal_func, + GBoxedCopyFunc key_copy_func, + GBoxedFreeFunc key_destroy_func, + GBoxedCopyFunc value_copy_func, + GBoxedFreeFunc value_free_func, + gint64 time_to_live_msec, + DzlTaskCacheCallback populate_callback, + gpointer populate_callback_data, + GDestroyNotify populate_callback_data_destroy); +DZL_AVAILABLE_IN_ALL +void dzl_task_cache_set_name (DzlTaskCache *self, + const gchar *name); +DZL_AVAILABLE_IN_ALL +void dzl_task_cache_get_async (DzlTaskCache *self, + gconstpointer key, + gboolean force_update, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +DZL_AVAILABLE_IN_ALL +gpointer dzl_task_cache_get_finish (DzlTaskCache *self, + GAsyncResult *result, + GError **error); +DZL_AVAILABLE_IN_ALL +gboolean dzl_task_cache_evict (DzlTaskCache *self, + gconstpointer key); +DZL_AVAILABLE_IN_ALL +void dzl_task_cache_evict_all (DzlTaskCache *self); +DZL_AVAILABLE_IN_ALL +gpointer dzl_task_cache_peek (DzlTaskCache *self, + gconstpointer key); +DZL_AVAILABLE_IN_ALL +GPtrArray *dzl_task_cache_get_values (DzlTaskCache *self); + +G_END_DECLS + +#endif /* DZL_TASK_CACHE_H */ diff --git a/src/cache/meson.build b/src/cache/meson.build new file mode 100644 index 0000000..a441a74 --- /dev/null +++ b/src/cache/meson.build @@ -0,0 +1,12 @@ +cache_headers = [ + 'dzl-task-cache.h', +] + +cache_sources = [ + 'dzl-task-cache.c', +] + +libdazzle_public_headers += files(cache_headers) +libdazzle_public_sources += files(cache_sources) + +install_headers(cache_headers, subdir: join_paths(libdazzle_header_subdir, 'cache')) diff --git a/src/dazzle.gresources.xml b/src/dazzle.gresources.xml new file mode 100644 index 0000000..a5061d4 --- /dev/null +++ b/src/dazzle.gresources.xml @@ -0,0 +1,66 @@ + + + + widgets/dzl-counters-window.ui + widgets/dzl-simple-popover.ui + widgets/dzl-empty-state.ui + menus/dzl-menu-button.ui + menus/dzl-menu-button-section.ui + widgets/dzl-pill-box.ui + prefs/dzl-preferences-file-chooser-button.ui + prefs/dzl-preferences-entry.ui + prefs/dzl-preferences-font-button.ui + prefs/dzl-preferences-group.ui + prefs/dzl-preferences-page.ui + prefs/dzl-preferences-spin-button.ui + prefs/dzl-preferences-switch.ui + prefs/dzl-preferences-view.ui + suggestions/dzl-suggestion-popover.ui + suggestions/dzl-suggestion-row.ui + shortcuts/dzl-shortcut-accel-dialog.ui + shortcuts/dzl-shortcut-theme-editor.ui + + + + shortcuts/enter-keyboard-shortcut.svg + + + + panel/panel-bottom-pane-symbolic.svg + panel/panel-left-pane-symbolic.svg + panel/panel-right-pane-symbolic.svg + + + + + ../data/themes/Arc.css + ../data/themes/Arc.css + ../data/themes/Arc.css + ../data/themes/Arc.css + ../data/themes/Arc.css + ../data/themes/Arc.css + ../data/themes/Arc/Arc-panels.css + + + ../data/themes/Adwaita.css + ../data/themes/Adwaita-dark.css + ../data/themes/Adwaita/Adwaita-panels.css + ../data/themes/Adwaita/Adwaita-graphs.css + ../data/themes/Adwaita/Adwaita-searchbar.css + ../data/themes/Adwaita/Adwaita-dark-graphs.css + ../data/themes/Adwaita/Adwaita-dark-searchbar.css + + + ../data/themes/shared.css + ../data/themes/shared/shared-graphs.css + ../data/themes/shared/shared-menus.css + ../data/themes/shared/shared-panels.css + ../data/themes/shared/shared-pathbar.css + ../data/themes/shared/shared-pillbox.css + ../data/themes/shared/shared-preferences.css + ../data/themes/shared/shared-progressbutton.css + ../data/themes/shared/shared-stacklist.css + ../data/themes/shared/shared-suggestions.css + + + diff --git a/src/dazzle.h b/src/dazzle.h new file mode 100644 index 0000000..0e08b7a --- /dev/null +++ b/src/dazzle.h @@ -0,0 +1,175 @@ +/* dazzle.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DAZZLE_H +#define DAZZLE_H + +#include + +G_BEGIN_DECLS + +#if !GTK_CHECK_VERSION(3, 22, 15) +# error "libdazzle requires gtk+-3.0 >= 3.22.15" +#endif + +#if !GLIB_CHECK_VERSION(2, 52, 0) +# error "libdazzle requires glib-2.0 >= 2.52.0" +#endif + +#define DAZZLE_INSIDE +#include "dzl-version.h" +#include "dzl-enums.h" +#include "actions/dzl-action-group.h" +#include "actions/dzl-child-property-action.h" +#include "actions/dzl-properties-group.h" +#include "actions/dzl-settings-flag-action.h" +#include "actions/dzl-widget-action-group.h" +#include "animation/dzl-animation.h" +#include "animation/dzl-box-theatric.h" +#include "app/dzl-application.h" +#include "app/dzl-application-window.h" +#include "bindings/dzl-binding-group.h" +#include "bindings/dzl-signal-group.h" +#include "cache/dzl-task-cache.h" +#include "files/dzl-directory-model.h" +#include "files/dzl-directory-reaper.h" +#include "files/dzl-file-transfer.h" +#include "files/dzl-recursive-file-monitor.h" +#include "graphing/dzl-cpu-graph.h" +#include "graphing/dzl-cpu-model.h" +#include "graphing/dzl-graph-column.h" +#include "graphing/dzl-graph-line-renderer.h" +#include "graphing/dzl-graph-model.h" +#include "graphing/dzl-graph-renderer.h" +#include "graphing/dzl-graph-view.h" +#include "menus/dzl-joined-menu.h" +#include "menus/dzl-menu-button.h" +#include "menus/dzl-menu-manager.h" +#include "panel/dzl-dock-bin-edge.h" +#include "panel/dzl-dock-bin.h" +#include "panel/dzl-dock-item.h" +#include "panel/dzl-dock-manager.h" +#include "panel/dzl-dock-overlay-edge.h" +#include "panel/dzl-dock-overlay.h" +#include "panel/dzl-dock-paned.h" +#include "panel/dzl-dock-revealer.h" +#include "panel/dzl-dock-stack.h" +#include "panel/dzl-dock-transient-grab.h" +#include "panel/dzl-dock-types.h" +#include "panel/dzl-dock-widget.h" +#include "panel/dzl-dock-window.h" +#include "panel/dzl-dock.h" +#include "panel/dzl-tab-strip.h" +#include "panel/dzl-tab.h" +#include "pathbar/dzl-path.h" +#include "pathbar/dzl-path-bar.h" +#include "pathbar/dzl-path-element.h" +#include "prefs/dzl-preferences-bin.h" +#include "prefs/dzl-preferences-entry.h" +#include "prefs/dzl-preferences-file-chooser-button.h" +#include "prefs/dzl-preferences-flow-box.h" +#include "prefs/dzl-preferences-font-button.h" +#include "prefs/dzl-preferences-group.h" +#include "prefs/dzl-preferences-page.h" +#include "prefs/dzl-preferences-spin-button.h" +#include "prefs/dzl-preferences-switch.h" +#include "prefs/dzl-preferences-view.h" +#include "prefs/dzl-preferences.h" +#include "search/dzl-fuzzy-index.h" +#include "search/dzl-fuzzy-index-builder.h" +#include "search/dzl-fuzzy-index-cursor.h" +#include "search/dzl-fuzzy-index-match.h" +#include "search/dzl-fuzzy-mutable-index.h" +#include "search/dzl-levenshtein.h" +#include "search/dzl-pattern-spec.h" +#include "search/dzl-trie.h" +#include "settings/dzl-settings-sandwich.h" +#include "shortcuts/dzl-shortcut-accel-dialog.h" +#include "shortcuts/dzl-shortcut-chord.h" +#include "shortcuts/dzl-shortcut-context.h" +#include "shortcuts/dzl-shortcut-controller.h" +#include "shortcuts/dzl-shortcut-label.h" +#include "shortcuts/dzl-shortcut-manager.h" +#include "shortcuts/dzl-shortcut-model.h" +#include "shortcuts/dzl-shortcut-phase.h" +#include "shortcuts/dzl-shortcut-simple-label.h" +#include "shortcuts/dzl-shortcut-theme-editor.h" +#include "shortcuts/dzl-shortcut-theme.h" +#include "shortcuts/dzl-shortcuts-group.h" +#include "shortcuts/dzl-shortcuts-section.h" +#include "shortcuts/dzl-shortcuts-shortcut.h" +#include "shortcuts/dzl-shortcuts-window.h" +#include "statemachine/dzl-state-machine-buildable.h" +#include "statemachine/dzl-state-machine.h" +#include "suggestions/dzl-suggestion-entry-buffer.h" +#include "suggestions/dzl-suggestion-entry.h" +#include "suggestions/dzl-suggestion-popover.h" +#include "suggestions/dzl-suggestion-row.h" +#include "suggestions/dzl-suggestion.h" +#include "theming/dzl-css-provider.h" +#include "theming/dzl-theme-manager.h" +#include "tree/dzl-list-store-adapter.h" +#include "tree/dzl-tree.h" +#include "tree/dzl-tree-builder.h" +#include "tree/dzl-tree-node.h" +#include "tree/dzl-tree-types.h" +#include "util/dzl-cairo.h" +#include "util/dzl-cancellable.h" +#include "util/dzl-counter.h" +#include "util/dzl-date-time.h" +#include "util/dzl-dnd.h" +#include "util/dzl-file-manager.h" +#include "util/dzl-gdk.h" +#include "util/dzl-gtk.h" +#include "util/dzl-heap.h" +#include "util/dzl-int-pair.h" +#include "util/dzl-macros.h" +#include "util/dzl-pango.h" +#include "util/dzl-rgba.h" +#include "util/dzl-ring.h" +#include "util/dzl-variant.h" +#include "widgets/dzl-bin.h" +#include "widgets/dzl-bolding-label.h" +#include "widgets/dzl-box.h" +#include "widgets/dzl-centering-bin.h" +#include "widgets/dzl-column-layout.h" +#include "widgets/dzl-counters-window.h" +#include "widgets/dzl-elastic-bin.h" +#include "widgets/dzl-empty-state.h" +#include "widgets/dzl-entry-box.h" +#include "widgets/dzl-file-chooser-entry.h" +#include "widgets/dzl-list-box.h" +#include "widgets/dzl-multi-paned.h" +#include "widgets/dzl-pill-box.h" +#include "widgets/dzl-priority-box.h" +#include "widgets/dzl-progress-button.h" +#include "widgets/dzl-progress-menu-button.h" +#include "widgets/dzl-progress-icon.h" +#include "widgets/dzl-radio-box.h" +#include "widgets/dzl-scrolled-window.h" +#include "widgets/dzl-search-bar.h" +#include "widgets/dzl-simple-label.h" +#include "widgets/dzl-simple-popover.h" +#include "widgets/dzl-slider.h" +#include "widgets/dzl-stack-list.h" +#include "widgets/dzl-three-grid.h" +#undef DAZZLE_INSIDE + +G_END_DECLS + +#endif /* DAZZLE_H */ diff --git a/src/dzl-debug.h.in b/src/dzl-debug.h.in new file mode 100644 index 0000000..c8286f3 --- /dev/null +++ b/src/dzl-debug.h.in @@ -0,0 +1,83 @@ +/* dzl-debug.h.in + * + * Copyright (C) 2013-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_DEBUG_H +#define DZL_DEBUG_H + +#include + +G_BEGIN_DECLS + +#ifndef DZL_ENABLE_TRACE +# define DZL_ENABLE_TRACE @ENABLE_TRACING@ +#endif +#if DZL_ENABLE_TRACE != 1 +# undef DZL_ENABLE_TRACE +#endif + +/** + * DZL_LOG_LEVEL_TRACE: (skip) + */ +#ifndef DZL_LOG_LEVEL_TRACE +# define DZL_LOG_LEVEL_TRACE ((GLogLevelFlags)(1 << G_LOG_LEVEL_USER_SHIFT)) +#endif + +#ifdef DZL_ENABLE_TRACE +# define DZL_TRACE_MSG(fmt, ...) \ + g_log(G_LOG_DOMAIN, DZL_LOG_LEVEL_TRACE, " MSG: %s():%d: " fmt, \ + G_STRFUNC, __LINE__, ##__VA_ARGS__) +# define DZL_PROBE \ + g_log(G_LOG_DOMAIN, DZL_LOG_LEVEL_TRACE, "PROBE: %s():%d", \ + G_STRFUNC, __LINE__) +# define DZL_TODO(_msg) \ + g_log(G_LOG_DOMAIN, DZL_LOG_LEVEL_TRACE, " TODO: %s():%d: %s", \ + G_STRFUNC, __LINE__, _msg) +# define DZL_ENTRY \ + g_log(G_LOG_DOMAIN, DZL_LOG_LEVEL_TRACE, "ENTRY: %s():%d", \ + G_STRFUNC, __LINE__) +# define DZL_EXIT \ + G_STMT_START { \ + g_log(G_LOG_DOMAIN, DZL_LOG_LEVEL_TRACE, " EXIT: %s():%d", \ + G_STRFUNC, __LINE__); \ + return; \ + } G_STMT_END +# define DZL_GOTO(_l) \ + G_STMT_START { \ + g_log(G_LOG_DOMAIN, DZL_LOG_LEVEL_TRACE, " GOTO: %s():%d ("#_l")", \ + G_STRFUNC, __LINE__); \ + goto _l; \ + } G_STMT_END +# define DZL_RETURN(_r) \ + G_STMT_START { \ + g_log(G_LOG_DOMAIN, DZL_LOG_LEVEL_TRACE, " EXIT: %s():%d ", \ + G_STRFUNC, __LINE__); \ + return _r; \ + } G_STMT_END +#else +# define DZL_TODO(_msg) +# define DZL_PROBE +# define DZL_TRACE_MSG(fmt, ...) +# define DZL_ENTRY +# define DZL_GOTO(_l) goto _l +# define DZL_EXIT return +# define DZL_RETURN(_r) return _r +#endif + +G_END_DECLS + +#endif /* DZL_DEBUG_H */ diff --git a/src/dzl-enums.c.in b/src/dzl-enums.c.in new file mode 100644 index 0000000..ce68f26 --- /dev/null +++ b/src/dzl-enums.c.in @@ -0,0 +1,41 @@ +/*** BEGIN file-header ***/ + +#include "config.h" + +#include "dzl-enums.h" + +#include "files/dzl-file-transfer.h" +#include "tree/dzl-tree-types.h" + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ +/* enumerations from "@filename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType +@enum_name@_get_type (void) +{ + static GType etype = 0; + if (G_UNLIKELY(etype == 0)) { + static const G@Type@Value values[] = { +/*** END value-header ***/ + +/*** BEGIN value-production ***/ + { @VALUENAME@, "@VALUENAME@", "@valuenick@" }, +/*** END value-production ***/ + +/*** BEGIN value-tail ***/ + { 0, NULL, NULL } + }; + etype = g_@type@_register_static (g_intern_static_string ("@EnumName@"), values); + } + return etype; +} + +/*** END value-tail ***/ + +/*** BEGIN file-tail ***/ + +/*** END file-tail ***/ diff --git a/src/dzl-enums.h.in b/src/dzl-enums.h.in new file mode 100644 index 0000000..815d002 --- /dev/null +++ b/src/dzl-enums.h.in @@ -0,0 +1,26 @@ +/*** BEGIN file-header ***/ +#ifndef __DZL_ENUMS_H__ +#define __DZL_ENUMS_H__ + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS +/*** END file-header ***/ + +/*** BEGIN file-production ***/ + +/* enumerations from "@filename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +DZL_AVAILABLE_IN_ALL GType @enum_name@_get_type (void); +#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()) +/*** END value-header ***/ + +/*** BEGIN file-tail ***/ +G_END_DECLS + +#endif /* __DZL_ENUMS_H__ */ +/*** END file-tail ***/ diff --git a/src/dzl-version-macros.h b/src/dzl-version-macros.h new file mode 100644 index 0000000..1ffee7b --- /dev/null +++ b/src/dzl-version-macros.h @@ -0,0 +1,131 @@ +/* dzl-version-macros.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_VERSION_MACROS_H +#define DZL_VERSION_MACROS_H + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#include + +#include "dzl-version.h" + +#ifndef _DZL_EXTERN +#define _DZL_EXTERN extern +#endif + +#ifdef DZL_DISABLE_DEPRECATION_WARNINGS +#define DZL_DEPRECATED _DZL_EXTERN +#define DZL_DEPRECATED_FOR(f) _DZL_EXTERN +#define DZL_UNAVAILABLE(maj,min) _DZL_EXTERN +#else +#define DZL_DEPRECATED G_DEPRECATED _DZL_EXTERN +#define DZL_DEPRECATED_FOR(f) G_DEPRECATED_FOR(f) _DZL_EXTERN +#define DZL_UNAVAILABLE(maj,min) G_UNAVAILABLE(maj,min) _DZL_EXTERN +#endif + +#define DZL_VERSION_3_28 (G_ENCODE_VERSION (3, 28)) + +#if (DZL_MINOR_VERSION == 99) +# define DZL_VERSION_CUR_STABLE (G_ENCODE_VERSION (DZL_MAJOR_VERSION + 1, 0)) +#elif (DZL_MINOR_VERSION % 2) +# define DZL_VERSION_CUR_STABLE (G_ENCODE_VERSION (DZL_MAJOR_VERSION, DZL_MINOR_VERSION + 1)) +#else +# define DZL_VERSION_CUR_STABLE (G_ENCODE_VERSION (DZL_MAJOR_VERSION, DZL_MINOR_VERSION)) +#endif + +#if (DZL_MINOR_VERSION == 99) +# define DZL_VERSION_PREV_STABLE (G_ENCODE_VERSION (DZL_MAJOR_VERSION + 1, 0)) +#elif (DZL_MINOR_VERSION % 2) +# define DZL_VERSION_PREV_STABLE (G_ENCODE_VERSION (DZL_MAJOR_VERSION, DZL_MINOR_VERSION - 1)) +#else +# define DZL_VERSION_PREV_STABLE (G_ENCODE_VERSION (DZL_MAJOR_VERSION, DZL_MINOR_VERSION - 2)) +#endif + +/** + * DZL_VERSION_MIN_REQUIRED: + * + * A macro that should be defined by the user prior to including + * the dazzle.h header. + * + * The definition should be one of the predefined DZL version + * macros: %DZL_VERSION_3_28, ... + * + * This macro defines the lower bound for the Dazzle API to use. + * + * If a function has been deprecated in a newer version of Dazzle, + * it is possible to use this symbol to avoid the compiler warnings + * without disabling warning for every deprecated function. + * + * Since: 3.28 + */ +#ifndef DZL_VERSION_MIN_REQUIRED +# define DZL_VERSION_MIN_REQUIRED (DZL_VERSION_CUR_STABLE) +#endif + +/** + * DZL_VERSION_MAX_ALLOWED: + * + * A macro that should be defined by the user prior to including + * the dazzle.h header. + + * The definition should be one of the predefined Dazzle version + * macros: %DZL_VERSION_1_0, %DZL_VERSION_1_2,... + * + * This macro defines the upper bound for the DZL API to use. + * + * If a function has been introduced in a newer version of Dazzle, + * it is possible to use this symbol to get compiler warnings when + * trying to use that function. + * + * Since: 3.28 + */ +#ifndef DZL_VERSION_MAX_ALLOWED +# if DZL_VERSION_MIN_REQUIRED > DZL_VERSION_PREV_STABLE +# define DZL_VERSION_MAX_ALLOWED (DZL_VERSION_MIN_REQUIRED) +# else +# define DZL_VERSION_MAX_ALLOWED (DZL_VERSION_CUR_STABLE) +# endif +#endif + +#if DZL_VERSION_MAX_ALLOWED < DZL_VERSION_MIN_REQUIRED +#error "DZL_VERSION_MAX_ALLOWED must be >= DZL_VERSION_MIN_REQUIRED" +#endif +#if DZL_VERSION_MIN_REQUIRED < DZL_VERSION_3_28 +#error "DZL_VERSION_MIN_REQUIRED must be >= DZL_VERSION_3_28" +#endif + +#define DZL_AVAILABLE_IN_ALL _DZL_EXTERN + +#if DZL_VERSION_MIN_REQUIRED >= DZL_VERSION_3_28 +# define DZL_DEPRECATED_IN_3_28 DZL_DEPRECATED +# define DZL_DEPRECATED_IN_3_28_FOR(f) DZL_DEPRECATED_FOR(f) +#else +# define DZL_DEPRECATED_IN_3_28 _DZL_EXTERN +# define DZL_DEPRECATED_IN_3_28_FOR(f) _DZL_EXTERN +#endif + +#if DZL_VERSION_MAX_ALLOWED < DZL_VERSION_3_28 +# define DZL_AVAILABLE_IN_3_28 DZL_UNAVAILABLE(3, 28) +#else +# define DZL_AVAILABLE_IN_3_28 _DZL_EXTERN +#endif + +#endif /* DZL_VERSION_MACROS_H */ diff --git a/src/dzl-version.h.in b/src/dzl-version.h.in new file mode 100644 index 0000000..b9296f4 --- /dev/null +++ b/src/dzl-version.h.in @@ -0,0 +1,96 @@ +/* dzl-version.h.in + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_VERSION_H +#define DZL_VERSION_H + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +#error "Only can be included directly." +#endif + +/** + * SECTION:dzl-version + * @short_description: Dazzle version checking + * + * Dazzle provides macros to check the version of the library at compile-time + */ + +/** + * DZL_MAJOR_VERSION: + * + * Dzl major version component (e.g. 1 if %DZL_VERSION is 1.2.3) + */ +#define DZL_MAJOR_VERSION (@DZL_MAJOR_VERSION@) + +/** + * DZL_MINOR_VERSION: + * + * Dzl minor version component (e.g. 2 if %DZL_VERSION is 1.2.3) + */ +#define DZL_MINOR_VERSION (@DZL_MINOR_VERSION@) + +/** + * DZL_MICRO_VERSION: + * + * Dzl micro version component (e.g. 3 if %DZL_VERSION is 1.2.3) + */ +#define DZL_MICRO_VERSION (@DZL_MICRO_VERSION@) + +/** + * DZL_VERSION + * + * Dzl version. + */ +#define DZL_VERSION (@DZL_VERSION@) + +/** + * DZL_VERSION_S: + * + * Dazzle version, encoded as a string, useful for printing and + * concatenation. + */ +#define DZL_VERSION_S "@DZL_VERSION@" + +#define DZL_ENCODE_VERSION(major,minor,micro) \ + ((major) << 24 | (minor) << 16 | (micro) << 8) + +/** + * DZL_VERSION_HEX: + * + * Dazzle version, encoded as an hexadecimal number, useful for + * integer comparisons. + */ +#define DZL_VERSION_HEX \ + (DZL_ENCODE_VERSION (DZL_MAJOR_VERSION, DZL_MINOR_VERSION, DZL_MICRO_VERSION)) + +/** + * DZL_CHECK_VERSION: + * @major: required major version + * @minor: required minor version + * @micro: required micro version + * + * Compile-time version checking. Evaluates to %TRUE if the version + * of dazzle is greater than the required one. + */ +#define DZL_CHECK_VERSION(major,minor,micro) \ + (DZL_MAJOR_VERSION > (major) || \ + (DZL_MAJOR_VERSION == (major) && DZL_MINOR_VERSION > (minor)) || \ + (DZL_MAJOR_VERSION == (major) && DZL_MINOR_VERSION == (minor) && \ + DZL_MICRO_VERSION >= (micro))) + +#endif /* DZL_VERSION_H */ diff --git a/src/files/dzl-directory-model.c b/src/files/dzl-directory-model.c new file mode 100644 index 0000000..ad19f4d --- /dev/null +++ b/src/files/dzl-directory-model.c @@ -0,0 +1,495 @@ +/* dzl-directory-model.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-directory-model" + +#include "config.h" + +#include + +#include "dzl-directory-model.h" + +#define NEXT_FILES_CHUNK_SIZE 25 + +struct _DzlDirectoryModel +{ + GObject parent_instance; + + GCancellable *cancellable; + GFile *directory; + GSequence *items; + GFileMonitor *monitor; + + DzlDirectoryModelVisibleFunc visible_func; + gpointer visible_func_data; + GDestroyNotify visible_func_destroy; +}; + +static void list_model_iface_init (GListModelInterface *iface); +static void dzl_directory_model_reload (DzlDirectoryModel *self); + +G_DEFINE_TYPE_EXTENDED (DzlDirectoryModel, dzl_directory_model, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)) + +enum { + PROP_0, + PROP_DIRECTORY, + LAST_PROP +}; + +static GParamSpec *gParamSpecs [LAST_PROP]; + +static gint +compare_display_name (gconstpointer a, + gconstpointer b, + gpointer data) +{ + GFileInfo *file_info_a = (GFileInfo *)a; + GFileInfo *file_info_b = (GFileInfo *)b; + const gchar *display_name_a = g_file_info_get_display_name (file_info_a); + const gchar *display_name_b = g_file_info_get_display_name (file_info_b); + g_autofree gchar *name_a = g_utf8_collate_key_for_filename (display_name_a, -1); + g_autofree gchar *name_b = g_utf8_collate_key_for_filename (display_name_b, -1); + + return g_utf8_collate (name_a, name_b); +} + +static gint +compare_directories_first (gconstpointer a, + gconstpointer b, + gpointer data) +{ + GFileInfo *file_info_a = (GFileInfo *)a; + GFileInfo *file_info_b = (GFileInfo *)b; + GFileType file_type_a = g_file_info_get_file_type (file_info_a); + GFileType file_type_b = g_file_info_get_file_type (file_info_b); + + if (file_type_a == file_type_b) + return compare_display_name (a, b, data); + + return (file_type_a == G_FILE_TYPE_DIRECTORY) ? -1 : 1; +} + +static void +dzl_directory_model_remove_all (DzlDirectoryModel *self) +{ + GSequence *seq; + guint length; + + g_assert (DZL_IS_DIRECTORY_MODEL (self)); + + length = g_sequence_get_length (self->items); + + if (length > 0) + { + seq = self->items; + self->items = g_sequence_new (g_object_unref); + g_list_model_items_changed (G_LIST_MODEL (self), 0, length, 0); + g_sequence_free (seq); + } +} + +static void +dzl_directory_model_take_item (DzlDirectoryModel *self, + GFileInfo *file_info) +{ + GSequenceIter *iter; + guint position; + + g_assert (DZL_IS_DIRECTORY_MODEL (self)); + g_assert (G_IS_FILE_INFO (file_info)); + + if ((self->visible_func != NULL) && + !self->visible_func (self, self->directory, file_info, self->visible_func_data)) + { + g_object_unref (file_info); + return; + } + + iter = g_sequence_insert_sorted (self->items, + file_info, + compare_directories_first, + NULL); + position = g_sequence_iter_get_position (iter); + g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1); +} + +static void +dzl_directory_model_next_files_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GFileEnumerator *enumerator = (GFileEnumerator *)object; + g_autoptr(GTask) task = user_data; + DzlDirectoryModel *self; + GList *files; + GList *iter; + + g_assert (G_IS_FILE_ENUMERATOR (enumerator)); + g_assert (G_IS_TASK (task)); + + if (!(files = g_file_enumerator_next_files_finish (enumerator, result, NULL))) + return; + + self = g_task_get_source_object (task); + + g_assert (DZL_IS_DIRECTORY_MODEL (self)); + + for (iter = files; iter; iter = iter->next) + { + GFileInfo *file_info = iter->data; + + dzl_directory_model_take_item (self, file_info); + } + + g_list_free (files); + + g_file_enumerator_next_files_async (enumerator, + NEXT_FILES_CHUNK_SIZE, + G_PRIORITY_LOW, + g_task_get_cancellable (task), + dzl_directory_model_next_files_cb, + g_object_ref (task)); +} + +static void +dzl_directory_model_enumerate_children_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GFile *directory = (GFile *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GFileEnumerator) enumerator = NULL; + + g_assert (G_IS_FILE (directory)); + g_assert (G_IS_TASK (task)); + + if (!(enumerator = g_file_enumerate_children_finish (directory, result, NULL))) + return; + + g_file_enumerator_next_files_async (enumerator, + NEXT_FILES_CHUNK_SIZE, + G_PRIORITY_LOW, + g_task_get_cancellable (task), + dzl_directory_model_next_files_cb, + g_object_ref (task)); +} + +static void +dzl_directory_model_remove_file (DzlDirectoryModel *self, + GFile *file) +{ + g_autofree gchar *name = NULL; + GSequenceIter *iter; + + g_assert (G_IS_FILE (file)); + + name = g_file_get_basename (file); + + /* + * We have to lookup linearly since the items will likely be + * sorted by name, directory, file-system ordering, or some + * combination thereof. + */ + + for (iter = g_sequence_get_begin_iter (self->items); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + GFileInfo *file_info = g_sequence_get (iter); + const gchar *file_info_name = g_file_info_get_name (file_info); + + if (0 == g_strcmp0 (file_info_name, name)) + { + guint position; + + position = g_sequence_iter_get_position (iter); + g_sequence_remove (iter); + g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 0); + break; + } + } +} + +static void +dzl_directory_model_directory_changed (DzlDirectoryModel *self, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + GFileMonitor *monitor) +{ + g_assert (DZL_IS_DIRECTORY_MODEL (self)); + + switch ((int)event_type) + { + case G_FILE_MONITOR_EVENT_CREATED: + /* + * TODO: incremental changes + * + * When adding, we need to first add the GFileInfo for the file with all + * of the attributes we load in the primary case. + */ + dzl_directory_model_reload (self); + break; + + case G_FILE_MONITOR_EVENT_DELETED: + dzl_directory_model_remove_file (self, file); + break; + + default: + break; + } +} + +static void +dzl_directory_model_reload (DzlDirectoryModel *self) +{ + g_assert (DZL_IS_DIRECTORY_MODEL (self)); + + if (self->monitor != NULL) + { + g_file_monitor_cancel (self->monitor); + g_signal_handlers_disconnect_by_func (self->monitor, + G_CALLBACK (dzl_directory_model_directory_changed), + self); + g_clear_object (&self->monitor); + } + + if (self->cancellable != NULL) + { + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + } + + dzl_directory_model_remove_all (self); + + if (self->directory != NULL) + { + g_autoptr(GTask) task = NULL; + + self->cancellable = g_cancellable_new (); + task = g_task_new (self, self->cancellable, NULL, NULL); + + g_file_enumerate_children_async (self->directory, + G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME"," + G_FILE_ATTRIBUTE_STANDARD_NAME"," + G_FILE_ATTRIBUTE_STANDARD_TYPE"," + G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON, + G_FILE_QUERY_INFO_NONE, + G_PRIORITY_LOW, + self->cancellable, + dzl_directory_model_enumerate_children_cb, + g_object_ref (task)); + + self->monitor = g_file_monitor_directory (self->directory, + G_FILE_MONITOR_NONE, + self->cancellable, + NULL); + + g_signal_connect_object (self->monitor, + "changed", + G_CALLBACK (dzl_directory_model_directory_changed), + self, + G_CONNECT_SWAPPED); + } +} + +static void +dzl_directory_model_finalize (GObject *object) +{ + DzlDirectoryModel *self = (DzlDirectoryModel *)object; + + g_clear_object (&self->cancellable); + g_clear_object (&self->directory); + g_clear_pointer (&self->items, g_sequence_free); + + if (self->visible_func_destroy) + self->visible_func_destroy (self->visible_func_data); + + G_OBJECT_CLASS (dzl_directory_model_parent_class)->finalize (object); +} + +static void +dzl_directory_model_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlDirectoryModel *self = DZL_DIRECTORY_MODEL (object); + + switch (prop_id) + { + case PROP_DIRECTORY: + g_value_set_object (value, dzl_directory_model_get_directory (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_directory_model_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlDirectoryModel *self = DZL_DIRECTORY_MODEL (object); + + switch (prop_id) + { + case PROP_DIRECTORY: + dzl_directory_model_set_directory (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_directory_model_class_init (DzlDirectoryModelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_directory_model_finalize; + object_class->get_property = dzl_directory_model_get_property; + object_class->set_property = dzl_directory_model_set_property; + + gParamSpecs [PROP_DIRECTORY] = + g_param_spec_object ("directory", + _("Directory"), + _("The directory to list files from."), + G_TYPE_FILE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs); +} + +static void +dzl_directory_model_init (DzlDirectoryModel *self) +{ + self->items = g_sequence_new (g_object_unref); +} + +/** + * dzl_directory_model_new: + * @directory: A #GFile + * + * Creates a new #DzlDirectoryModel using @directory as the directory to monitor. + * + * Returns: (transfer full): A newly created #DzlDirectoryModel + */ +GListModel * +dzl_directory_model_new (GFile *directory) +{ + g_return_val_if_fail (G_IS_FILE (directory), NULL); + + return g_object_new (DZL_TYPE_DIRECTORY_MODEL, + "directory", directory, + NULL); +} + +/** + * dzl_directory_model_get_directory: + * @self: a #DzlDirectoryModel + * + * Gets the directory the model is observing. + * + * Returns: (transfer none): A #GFile + */ +GFile * +dzl_directory_model_get_directory (DzlDirectoryModel *self) +{ + g_return_val_if_fail (DZL_IS_DIRECTORY_MODEL (self), NULL); + + return self->directory; +} + +void +dzl_directory_model_set_directory (DzlDirectoryModel *self, + GFile *directory) +{ + g_return_if_fail (DZL_IS_DIRECTORY_MODEL (self)); + g_return_if_fail (!directory || G_IS_FILE (directory)); + + if (g_set_object (&self->directory, directory)) + { + dzl_directory_model_reload (self); + g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_DIRECTORY]); + } +} + +static guint +dzl_directory_model_get_n_items (GListModel *model) +{ + DzlDirectoryModel *self = (DzlDirectoryModel *)model; + + g_return_val_if_fail (DZL_IS_DIRECTORY_MODEL (self), 0); + + return g_sequence_get_length (self->items); +} + +static GType +dzl_directory_model_get_item_type (GListModel *model) +{ + return G_TYPE_FILE_INFO; +} + +static gpointer +dzl_directory_model_get_item (GListModel *model, + guint position) +{ + DzlDirectoryModel *self = (DzlDirectoryModel *)model; + GSequenceIter *iter; + gpointer ret; + + g_return_val_if_fail (DZL_IS_DIRECTORY_MODEL (self), NULL); + + if ((iter = g_sequence_get_iter_at_pos (self->items, position)) && + (ret = g_sequence_get (iter))) + return g_object_ref (ret); + + return NULL; +} + +static void +list_model_iface_init (GListModelInterface *iface) +{ + iface->get_n_items = dzl_directory_model_get_n_items; + iface->get_item = dzl_directory_model_get_item; + iface->get_item_type = dzl_directory_model_get_item_type; +} + +void +dzl_directory_model_set_visible_func (DzlDirectoryModel *self, + DzlDirectoryModelVisibleFunc visible_func, + gpointer user_data, + GDestroyNotify user_data_free_func) +{ + g_return_if_fail (DZL_IS_DIRECTORY_MODEL (self)); + + if (self->visible_func_destroy != NULL) + self->visible_func_destroy (self->visible_func_data); + + self->visible_func = visible_func; + self->visible_func_data = user_data; + self->visible_func_destroy = user_data_free_func; + + dzl_directory_model_reload (self); +} diff --git a/src/files/dzl-directory-model.h b/src/files/dzl-directory-model.h new file mode 100644 index 0000000..cebd23d --- /dev/null +++ b/src/files/dzl-directory-model.h @@ -0,0 +1,53 @@ +/* dzl-directory-model.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_DIRECTORY_MODEL_H +#define DZL_DIRECTORY_MODEL_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_DIRECTORY_MODEL (dzl_directory_model_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlDirectoryModel, dzl_directory_model, DZL, DIRECTORY_MODEL, GObject) + +typedef gboolean (*DzlDirectoryModelVisibleFunc) (DzlDirectoryModel *self, + GFile *directory, + GFileInfo *file_info, + gpointer user_data); + +DZL_AVAILABLE_IN_ALL +GListModel *dzl_directory_model_new (GFile *directory); +DZL_AVAILABLE_IN_ALL +GFile *dzl_directory_model_get_directory (DzlDirectoryModel *self); +DZL_AVAILABLE_IN_ALL +void dzl_directory_model_set_directory (DzlDirectoryModel *self, + GFile *directory); +DZL_AVAILABLE_IN_ALL +void dzl_directory_model_set_visible_func (DzlDirectoryModel *self, + DzlDirectoryModelVisibleFunc visible_func, + gpointer user_data, + GDestroyNotify user_data_free_func); + +G_END_DECLS + +#endif /* DZL_DIRECTORY_MODEL_H */ diff --git a/src/files/dzl-directory-reaper.c b/src/files/dzl-directory-reaper.c new file mode 100644 index 0000000..b6219be --- /dev/null +++ b/src/files/dzl-directory-reaper.c @@ -0,0 +1,451 @@ +/* dzl-directory-reaper.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-directory-reaper" + +#include "config.h" + +#include "files/dzl-directory-reaper.h" + +typedef enum +{ + PATTERN_FILE, + PATTERN_GLOB, +} PatternType; + +typedef struct +{ + PatternType type; + GTimeSpan min_age; + union { + struct { + GFile *directory; + gchar *glob; + } glob; + struct { + GFile *file; + } file; + }; +} Pattern; + +struct _DzlDirectoryReaper +{ + GObject parent_instance; + GArray *patterns; +}; + +G_DEFINE_TYPE (DzlDirectoryReaper, dzl_directory_reaper, G_TYPE_OBJECT) + +static void +clear_pattern (gpointer data) +{ + Pattern *p = data; + + switch (p->type) + { + case PATTERN_GLOB: + g_clear_object (&p->glob.directory); + g_clear_pointer (&p->glob.glob, g_free); + break; + + case PATTERN_FILE: + g_clear_object (&p->file.file); + break; + + default: + g_assert_not_reached (); + } +} + +static void +dzl_directory_reaper_finalize (GObject *object) +{ + DzlDirectoryReaper *self = (DzlDirectoryReaper *)object; + + g_clear_pointer (&self->patterns, g_array_unref); + + G_OBJECT_CLASS (dzl_directory_reaper_parent_class)->finalize (object); +} + +static void +dzl_directory_reaper_class_init (DzlDirectoryReaperClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_directory_reaper_finalize; +} + +static void +dzl_directory_reaper_init (DzlDirectoryReaper *self) +{ + self->patterns = g_array_new (FALSE, FALSE, sizeof (Pattern)); + g_array_set_clear_func (self->patterns, clear_pattern); +} + +void +dzl_directory_reaper_add_directory (DzlDirectoryReaper *self, + GFile *directory, + GTimeSpan min_age) +{ + g_return_if_fail (DZL_IS_DIRECTORY_REAPER (self)); + g_return_if_fail (G_IS_FILE (directory)); + + dzl_directory_reaper_add_glob (self, directory, NULL, min_age); +} + +void +dzl_directory_reaper_add_glob (DzlDirectoryReaper *self, + GFile *directory, + const gchar *glob, + GTimeSpan min_age) +{ + Pattern p = { 0 }; + + g_return_if_fail (DZL_IS_DIRECTORY_REAPER (self)); + g_return_if_fail (G_IS_FILE (directory)); + + if (glob == NULL) + glob = "*"; + + p.type = PATTERN_GLOB; + p.min_age = ABS (min_age); + p.glob.directory = g_object_ref (directory); + p.glob.glob = g_strdup (glob); + + g_array_append_val (self->patterns, p); +} + +void +dzl_directory_reaper_add_file (DzlDirectoryReaper *self, + GFile *file, + GTimeSpan min_age) +{ + Pattern p = { 0 }; + + g_return_if_fail (DZL_IS_DIRECTORY_REAPER (self)); + g_return_if_fail (G_IS_FILE (file)); + + p.type = PATTERN_FILE; + p.min_age = ABS (min_age); + p.file.file = g_object_ref (file); + + g_array_append_val (self->patterns, p); +} + +DzlDirectoryReaper * +dzl_directory_reaper_new (void) +{ + return g_object_new (DZL_TYPE_DIRECTORY_REAPER, NULL); +} + +static gboolean +remove_directory_with_children (GFile *file, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFileEnumerator) enumerator = NULL; + g_autoptr(GError) enum_error = NULL; + g_autofree gchar *uri = NULL; + gpointer infoptr; + + g_assert (G_IS_FILE (file)); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + uri = g_file_get_uri (file); + g_debug ("Removing uri recursively \"%s\"", uri); + + enumerator = g_file_enumerate_children (file, + G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK"," + G_FILE_ATTRIBUTE_STANDARD_NAME"," + G_FILE_ATTRIBUTE_STANDARD_TYPE"," + G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable, + &enum_error); + + + if (enumerator == NULL) + { + /* If the directory does not exist, nothing to do */ + if (g_error_matches (enum_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + return FALSE; + } + + g_assert (enum_error == NULL); + + while (NULL != (infoptr = g_file_enumerator_next_file (enumerator, cancellable, &enum_error))) + { + g_autoptr(GFileInfo) info = infoptr; + g_autoptr(GFile) child = g_file_enumerator_get_child (enumerator, info); + GFileType file_type = g_file_info_get_file_type (info); + + if (!g_file_info_get_is_symlink (info) && file_type == G_FILE_TYPE_DIRECTORY) + { + if (!remove_directory_with_children (child, cancellable, error)) + return FALSE; + } + + if (!g_file_delete (child, cancellable, error)) + return FALSE; + } + + if (enum_error != NULL) + { + g_propagate_error (error, g_steal_pointer (&enum_error)); + return FALSE; + } + + if (!g_file_enumerator_close (enumerator, cancellable, error)) + return FALSE; + + return TRUE; +} + +static void +dzl_directory_reaper_execute_worker (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GArray *patterns = task_data; + gint64 now = g_get_real_time (); + + g_assert (G_IS_TASK (task)); + g_assert (DZL_IS_DIRECTORY_REAPER (source_object)); + g_assert (patterns != NULL); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + for (guint i = 0; i < patterns->len; i++) + { + const Pattern *p = &g_array_index (patterns, Pattern, i); + g_autoptr(GFileInfo) info = NULL; + g_autoptr(GFileInfo) dir_info = NULL; + g_autoptr(GPatternSpec) spec = NULL; + g_autoptr(GFileEnumerator) enumerator = NULL; + g_autoptr(GError) error = NULL; + guint64 v64; + + switch (p->type) + { + case PATTERN_FILE: + + info = g_file_query_info (p->file.file, + G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable, + &error); + + if (info == NULL) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + g_warning ("%s", error->message); + break; + } + + v64 = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED); + + /* mtime is in seconds */ + v64 *= G_USEC_PER_SEC; + + if (v64 < now - p->min_age) + { + if (!g_file_delete (p->file.file, cancellable, &error)) + g_warning ("%s", error->message); + } + + break; + + case PATTERN_GLOB: + + spec = g_pattern_spec_new (p->glob.glob); + + if (spec == NULL) + { + g_warning ("Invalid pattern spec \"%s\"", p->glob.glob); + break; + } + + dir_info = g_file_query_info (p->glob.directory, + G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK"," + G_FILE_ATTRIBUTE_STANDARD_TYPE",", + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable, + &error); + + if (dir_info == NULL) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + g_warning ("%s", error->message); + break; + } + + /* Do not follow through symlinks. */ + if (g_file_info_get_is_symlink (dir_info) || + g_file_info_get_file_type (dir_info) != G_FILE_TYPE_DIRECTORY) + break; + + enumerator = g_file_enumerate_children (p->glob.directory, + G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK"," + G_FILE_ATTRIBUTE_STANDARD_NAME"," + G_FILE_ATTRIBUTE_STANDARD_TYPE"," + G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable, + &error); + + if (enumerator == NULL) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + g_warning ("%s", error->message); + break; + } + + while (NULL != (info = g_file_enumerator_next_file (enumerator, cancellable, NULL))) + { + v64 = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED); + + /* mtime is in seconds */ + v64 *= G_USEC_PER_SEC; + + if (v64 < now - p->min_age) + { + g_autoptr(GFile) file = g_file_enumerator_get_child (enumerator, info); + GFileType file_type = g_file_info_get_file_type (info); + + if (g_file_info_get_is_symlink (info) || file_type != G_FILE_TYPE_DIRECTORY) + { + if (!g_file_delete (file, cancellable, &error)) + { + g_warning ("%s", error->message); + g_clear_error (&error); + } + } + else + { + g_assert (file_type == G_FILE_TYPE_DIRECTORY); + + if (!remove_directory_with_children (file, cancellable, &error) || + !g_file_delete (file, cancellable, &error)) + { + g_warning ("%s", error->message); + g_clear_error (&error); + } + } + } + + g_clear_object (&info); + } + + break; + + default: + g_assert_not_reached (); + } + } + + g_task_return_boolean (task, TRUE); +} + +static GArray * +dzl_directory_reaper_copy_state (DzlDirectoryReaper *self) +{ + g_autoptr(GArray) copy = NULL; + + g_assert (DZL_IS_DIRECTORY_REAPER (self)); + g_assert (self->patterns != NULL); + + copy = g_array_new (FALSE, FALSE, sizeof (Pattern)); + g_array_set_clear_func (copy, clear_pattern); + + for (guint i = 0; i < self->patterns->len; i++) + { + Pattern p = g_array_index (self->patterns, Pattern, i); + + switch (p.type) + { + case PATTERN_GLOB: + p.glob.directory = g_object_ref (p.glob.directory); + p.glob.glob = g_strdup (p.glob.glob); + break; + + case PATTERN_FILE: + p.file.file = g_object_ref (p.file.file); + break; + + default: + g_assert_not_reached (); + } + + g_array_append_val (copy, p); + } + + return g_steal_pointer (©); +} + +void +dzl_directory_reaper_execute_async (DzlDirectoryReaper *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GArray) copy = NULL; + + g_return_if_fail (DZL_IS_DIRECTORY_REAPER (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + copy = dzl_directory_reaper_copy_state (self); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, dzl_directory_reaper_execute_async); + g_task_set_task_data (task, g_steal_pointer (©), (GDestroyNotify)g_array_unref); + g_task_run_in_thread (task, dzl_directory_reaper_execute_worker); +} + +gboolean +dzl_directory_reaper_execute_finish (DzlDirectoryReaper *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (DZL_IS_DIRECTORY_REAPER (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +dzl_directory_reaper_execute (DzlDirectoryReaper *self, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GArray) copy = NULL; + + g_return_val_if_fail (DZL_IS_DIRECTORY_REAPER (self), FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + copy = dzl_directory_reaper_copy_state (self); + + task = g_task_new (self, cancellable, NULL, NULL); + g_task_set_source_tag (task, dzl_directory_reaper_execute); + g_task_set_task_data (task, g_steal_pointer (©), (GDestroyNotify)g_array_unref); + g_task_run_in_thread_sync (task, dzl_directory_reaper_execute_worker); + + return g_task_propagate_boolean (task, error); +} diff --git a/src/files/dzl-directory-reaper.h b/src/files/dzl-directory-reaper.h new file mode 100644 index 0000000..1f2edea --- /dev/null +++ b/src/files/dzl-directory-reaper.h @@ -0,0 +1,64 @@ +/* dzl-directory-reaper.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_DIRECTORY_REAPER_H +#define DZL_DIRECTORY_REAPER_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_DIRECTORY_REAPER (dzl_directory_reaper_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlDirectoryReaper, dzl_directory_reaper, DZL, DIRECTORY_REAPER, GObject) + +DZL_AVAILABLE_IN_ALL +DzlDirectoryReaper *dzl_directory_reaper_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_directory_reaper_add_directory (DzlDirectoryReaper *self, + GFile *directory, + GTimeSpan min_age); +DZL_AVAILABLE_IN_ALL +void dzl_directory_reaper_add_file (DzlDirectoryReaper *self, + GFile *file, + GTimeSpan min_age); +DZL_AVAILABLE_IN_ALL +void dzl_directory_reaper_add_glob (DzlDirectoryReaper *self, + GFile *directory, + const gchar *glob, + GTimeSpan min_age); +DZL_AVAILABLE_IN_ALL +gboolean dzl_directory_reaper_execute (DzlDirectoryReaper *self, + GCancellable *cancellable, + GError **error); +DZL_AVAILABLE_IN_ALL +void dzl_directory_reaper_execute_async (DzlDirectoryReaper *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +DZL_AVAILABLE_IN_ALL +gboolean dzl_directory_reaper_execute_finish (DzlDirectoryReaper *self, + GAsyncResult *result, + GError **error); + +G_END_DECLS + +#endif /* DZL_DIRECTORY_REAPER_H */ diff --git a/src/files/dzl-file-transfer.c b/src/files/dzl-file-transfer.c new file mode 100644 index 0000000..d2a8ef5 --- /dev/null +++ b/src/files/dzl-file-transfer.c @@ -0,0 +1,724 @@ +/* dzl-file-transfer.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-file-transfer" + +#include "config.h" + +#include "dzl-debug.h" +#include "dzl-enums.h" + +#include "files/dzl-directory-reaper.h" +#include "files/dzl-file-transfer.h" +#include "util/dzl-macros.h" + +#define QUERY_ATTRS (G_FILE_ATTRIBUTE_STANDARD_NAME"," \ + G_FILE_ATTRIBUTE_STANDARD_TYPE"," \ + G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK"," \ + G_FILE_ATTRIBUTE_STANDARD_SIZE) +#define QUERY_FLAGS (G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS) + +typedef struct +{ + GPtrArray *opers; + + DzlFileTransferStat stat_buf; + + DzlFileTransferFlags flags; + + gint64 last_num_bytes; + + guint executed : 1; +} DzlFileTransferPrivate; + +typedef struct +{ + /* Unowned pointers */ + DzlFileTransfer *self; + GCancellable *cancellable; + + /* Owned pointers */ + GFile *src; + GFile *dst; + GError *error; + + DzlFileTransferFlags flags; +} Oper; + +typedef void (*FileWalkCallback) (GFile *file, + GFileInfo *child_info, + gpointer user_data); + +enum { + PROP_0, + PROP_FLAGS, + PROP_PROGRESS, + N_PROPS +}; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlFileTransfer, dzl_file_transfer, G_TYPE_OBJECT) + +static GParamSpec *properties [N_PROPS]; + +static void +oper_free (gpointer data) +{ + Oper *oper = data; + + oper->self = NULL; + oper->cancellable = NULL; + + g_clear_object (&oper->src); + g_clear_object (&oper->dst); + g_clear_error (&oper->error); + + g_slice_free (Oper, oper); +} + +static void +dzl_file_transfer_finalize (GObject *object) +{ + DzlFileTransfer *self = (DzlFileTransfer *)object; + DzlFileTransferPrivate *priv = dzl_file_transfer_get_instance_private (self); + + g_clear_pointer (&priv->opers, g_ptr_array_unref); + + G_OBJECT_CLASS (dzl_file_transfer_parent_class)->finalize (object); +} + +static void +dzl_file_transfer_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlFileTransfer *self = DZL_FILE_TRANSFER (object); + + switch (prop_id) + { + case PROP_FLAGS: + g_value_set_flags (value, dzl_file_transfer_get_flags (self)); + break; + + case PROP_PROGRESS: + g_value_set_double (value, dzl_file_transfer_get_progress (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_file_transfer_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlFileTransfer *self = DZL_FILE_TRANSFER (object); + + switch (prop_id) + { + case PROP_FLAGS: + dzl_file_transfer_set_flags (self, g_value_get_flags (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_file_transfer_class_init (DzlFileTransferClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_file_transfer_finalize; + object_class->get_property = dzl_file_transfer_get_property; + object_class->set_property = dzl_file_transfer_set_property; + + properties [PROP_FLAGS] = + g_param_spec_flags ("flags", + "Flags", + "The transfer flags for the operation", + DZL_TYPE_FILE_TRANSFER_FLAGS, + DZL_FILE_TRANSFER_FLAGS_NONE, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_PROGRESS] = + g_param_spec_double ("progress", + "Progress", + "The transfer progress, from 0 to 1", + 0.0, 1.0, 0.0, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_file_transfer_init (DzlFileTransfer *self) +{ + DzlFileTransferPrivate *priv = dzl_file_transfer_get_instance_private (self); + + priv->opers = g_ptr_array_new_with_free_func (oper_free); +} + +DzlFileTransfer * +dzl_file_transfer_new (void) +{ + return g_object_new (DZL_TYPE_FILE_TRANSFER, NULL); +} + +void +dzl_file_transfer_add (DzlFileTransfer *self, + GFile *src, + GFile *dst) +{ + DzlFileTransferPrivate *priv = dzl_file_transfer_get_instance_private (self); + Oper *oper; + + DZL_ENTRY; + + g_return_if_fail (DZL_IS_FILE_TRANSFER (self)); + g_return_if_fail (G_IS_FILE (src)); + g_return_if_fail (G_IS_FILE (dst)); + + if (priv->executed) + { + g_warning ("Cannot add files to transfer after executing"); + DZL_EXIT; + } + + if (g_file_equal (src, dst)) + { + g_warning ("Source and destination cannot be the same"); + DZL_EXIT; + } + + if (g_file_has_prefix (dst, src)) + { + g_warning ("Destination cannot be within source"); + DZL_EXIT; + } + + oper = g_slice_new0 (Oper); + oper->src = g_object_ref (src); + oper->dst = g_object_ref (dst); + oper->self = self; + + g_assert (priv->opers != NULL); + + g_ptr_array_add (priv->opers, oper); + + DZL_EXIT; +} + +DzlFileTransferFlags +dzl_file_transfer_get_flags (DzlFileTransfer *self) +{ + DzlFileTransferPrivate *priv = dzl_file_transfer_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_FILE_TRANSFER (self), 0); + + return priv->flags; +} + +void +dzl_file_transfer_set_flags (DzlFileTransfer *self, + DzlFileTransferFlags flags) +{ + DzlFileTransferPrivate *priv = dzl_file_transfer_get_instance_private (self); + + g_return_if_fail (DZL_IS_FILE_TRANSFER (self)); + + if (priv->executed) + { + g_warning ("Cannot set flags after executing transfer"); + return; + } + + if (priv->flags != flags) + { + priv->flags = flags; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FLAGS]); + } +} + +gdouble +dzl_file_transfer_get_progress (DzlFileTransfer *self) +{ + DzlFileTransferPrivate *priv = dzl_file_transfer_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_FILE_TRANSFER (self), 0.0); + + if (priv->stat_buf.n_bytes_total != 0) + return (gdouble)priv->stat_buf.n_bytes / (gdouble)priv->stat_buf.n_bytes_total; + + return 0.0; +} + +static void +file_walk_full (GFile *parent, + GFileInfo *info, + GCancellable *cancellable, + FileWalkCallback callback, + gpointer user_data) +{ + g_assert (!parent || G_IS_FILE (parent)); + g_assert (G_IS_FILE_INFO (info)); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_assert (callback != NULL); + + if (g_cancellable_is_cancelled (cancellable)) + return; + + callback (parent, info, user_data); + + if (g_file_info_get_is_symlink (info)) + return; + + if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) + { + g_autoptr(GFileEnumerator) enumerator = NULL; + g_autoptr(GFile) child = NULL; + const gchar *name = g_file_info_get_name (info); + + if (name == NULL) + return; + + child = g_file_get_child (parent, name); + enumerator = g_file_enumerate_children (child, QUERY_ATTRS, QUERY_FLAGS, cancellable, NULL); + + if (enumerator != NULL) + { + gpointer infoptr; + + while (NULL != (infoptr = g_file_enumerator_next_file (enumerator, cancellable, NULL))) + { + g_autoptr(GFileInfo) grandchild_info = infoptr; + file_walk_full (child, grandchild_info, cancellable, callback, user_data); + } + + g_file_enumerator_close (enumerator, cancellable, NULL); + } + } +} + +static void +file_walk (GFile *root, + GCancellable *cancellable, + FileWalkCallback callback, + gpointer user_data) +{ + g_autoptr(GFile) parent = NULL; + g_autoptr(GFileInfo) info = NULL; + + g_assert (G_IS_FILE (root)); + g_assert (callback != NULL); + + parent = g_file_get_parent (root); + if (g_file_equal (root, parent)) + g_clear_object (&parent); + + info = g_file_query_info (root, QUERY_ATTRS, QUERY_FLAGS, cancellable, NULL); + if (info != NULL) + file_walk_full (parent, info, cancellable, callback, user_data); +} + +static void +handle_preflight_cb (GFile *file, + GFileInfo *child_info, + gpointer user_data) +{ + DzlFileTransferStat *stat_buf = user_data; + GFileType file_type; + + g_assert (G_IS_FILE (file)); + g_assert (G_IS_FILE_INFO (child_info)); + g_assert (stat_buf != NULL); + + file_type = g_file_info_get_file_type (child_info); + + if (file_type == G_FILE_TYPE_DIRECTORY) + { + stat_buf->n_dirs_total++; + } + else if (file_type == G_FILE_TYPE_REGULAR) + { + stat_buf->n_files_total++; + stat_buf->n_bytes_total += g_file_info_get_size (child_info); + } +} + +static void +handle_preflight (DzlFileTransfer *self, + GPtrArray *opers, + GCancellable *cancellable) +{ + DzlFileTransferPrivate *priv = dzl_file_transfer_get_instance_private (self); + + g_assert (DZL_IS_FILE_TRANSFER (self)); + g_assert (opers != NULL); + + if (g_cancellable_is_cancelled (cancellable)) + return; + + for (guint i = 0; i < opers->len; i++) + { + Oper *oper = g_ptr_array_index (opers, i); + + g_assert (oper != NULL); + g_assert (DZL_IS_FILE_TRANSFER (oper->self)); + g_assert (G_IS_FILE (oper->src)); + g_assert (G_IS_FILE (oper->dst)); + + file_walk (oper->src, cancellable, handle_preflight_cb, &priv->stat_buf); + + if (oper->error != NULL) + break; + } +} + +static void +dzl_file_transfer_progress_cb (goffset current_num_bytes, + goffset total_num_bytes, + gpointer user_data) +{ + DzlFileTransfer *self = user_data; + DzlFileTransferPrivate *priv = dzl_file_transfer_get_instance_private (self); + + priv->stat_buf.n_bytes += (current_num_bytes - priv->last_num_bytes); +} + +static void +handle_copy_cb (GFile *file, + GFileInfo *child_info, + gpointer user_data) +{ + DzlFileTransferPrivate *priv; + g_autoptr(GFile) src = NULL; + g_autoptr(GFile) dst = NULL; + const gchar *name; + Oper *oper = user_data; + GFileType file_type; + + g_assert (DZL_IS_FILE_TRANSFER (oper->self)); + g_assert (G_IS_FILE (oper->src)); + g_assert (G_IS_FILE (oper->dst)); + g_assert (G_IS_FILE (file)); + g_assert (G_IS_FILE_INFO (child_info)); + + if (oper->error != NULL) + return; + + if (g_cancellable_is_cancelled (oper->cancellable)) + return; + + priv = dzl_file_transfer_get_instance_private (oper->self); + + file_type = g_file_info_get_file_type (child_info); + name = g_file_info_get_name (child_info); + + if (name == NULL) + return; + + src = g_file_get_child (file, name); + + if (!g_file_equal (oper->src, src)) + { + g_autofree gchar *relative = NULL; + + relative = g_file_get_relative_path (oper->src, src); + dst = g_file_get_child (oper->dst, relative); + } + else + { + dst = g_object_ref (oper->dst); + } + + priv->last_num_bytes = 0; + + switch (file_type) + { + case G_FILE_TYPE_DIRECTORY: + g_file_make_directory_with_parents (dst, oper->cancellable, &oper->error); + break; + + case G_FILE_TYPE_REGULAR: + case G_FILE_TYPE_SPECIAL: + case G_FILE_TYPE_SHORTCUT: + case G_FILE_TYPE_SYMBOLIC_LINK: + /* Try to use g_file_move() when we can */ + if ((oper->flags & DZL_FILE_TRANSFER_FLAGS_MOVE) != 0) + g_file_move (src, dst, + G_FILE_COPY_NOFOLLOW_SYMLINKS | G_FILE_COPY_ALL_METADATA, + oper->cancellable, + dzl_file_transfer_progress_cb, + oper->self, + &oper->error); + else + g_file_copy (src, dst, + G_FILE_COPY_NOFOLLOW_SYMLINKS | G_FILE_COPY_ALL_METADATA, + oper->cancellable, + dzl_file_transfer_progress_cb, + oper->self, + &oper->error); + break; + + case G_FILE_TYPE_UNKNOWN: + case G_FILE_TYPE_MOUNTABLE: + default: + break; + } +} + +static void +handle_copy (DzlFileTransfer *self, + GPtrArray *opers, + GCancellable *cancellable) +{ + g_assert (DZL_IS_FILE_TRANSFER (self)); + g_assert (opers != NULL); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + if (g_cancellable_is_cancelled (cancellable)) + return; + + for (guint i = 0; i < opers->len; i++) + { + Oper *oper = g_ptr_array_index (opers, i); + + g_assert (oper != NULL); + g_assert (G_IS_FILE (oper->src)); + g_assert (G_IS_FILE (oper->dst)); + + oper->self = self; + oper->cancellable = cancellable; + + if (oper->error == NULL) + { + file_walk (oper->src, cancellable, handle_copy_cb, oper); + + if (oper->error != NULL) + break; + } + } +} + +static void +handle_removal (DzlFileTransfer *self, + GPtrArray *opers, + GCancellable *cancellable) +{ + g_autoptr(DzlDirectoryReaper) reaper = NULL; + + g_assert (DZL_IS_FILE_TRANSFER (self)); + g_assert (opers != NULL); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + if (g_cancellable_is_cancelled (cancellable)) + return; + + reaper = dzl_directory_reaper_new (); + + for (guint i = 0; i < opers->len; i++) + { + Oper *oper = g_ptr_array_index (opers, i); + + g_assert (oper != NULL); + g_assert (G_IS_FILE (oper->src)); + g_assert (G_IS_FILE (oper->dst)); + + /* Don't delete anything if there was a failure */ + if (oper->error != NULL) + return; + + if (g_file_query_file_type (oper->src, 0, NULL) == G_FILE_TYPE_DIRECTORY) + dzl_directory_reaper_add_directory (reaper, oper->src, 0); + + dzl_directory_reaper_add_file (reaper, oper->src, 0); + } + + dzl_directory_reaper_execute (reaper, cancellable, NULL); +} + +static void +dzl_file_transfer_worker (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + DzlFileTransfer *self = source_object; + DzlFileTransferPrivate *priv = dzl_file_transfer_get_instance_private (self); + GPtrArray *opers = task_data; + + DZL_ENTRY; + + g_assert (G_IS_TASK (task)); + g_assert (DZL_IS_FILE_TRANSFER (self)); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_assert (opers != NULL); + + /* TODO: Start GSource for notifies */ + + for (guint i = 0; i < opers->len; i++) + { + Oper *oper = g_ptr_array_index (opers, i); + + oper->self = self; + oper->cancellable = cancellable; + oper->flags = priv->flags; + } + + handle_preflight (self, opers, cancellable); + handle_copy (self, opers, cancellable); + if ((priv->flags & DZL_FILE_TRANSFER_FLAGS_MOVE) != 0) + handle_removal (self, opers, cancellable); + + for (guint i = 0; i < opers->len; i++) + { + Oper *oper = g_ptr_array_index (opers, i); + + if (oper->error != NULL) + { + g_task_return_error (task, g_steal_pointer (&oper->error)); + DZL_EXIT; + } + } + + g_task_return_boolean (task, TRUE); + + /* TODO: Stop GSource for notifies */ + + DZL_EXIT; +} + +gboolean +dzl_file_transfer_execute (DzlFileTransfer *self, + gint io_priority, + GCancellable *cancellable, + GError **error) +{ + DzlFileTransferPrivate *priv = dzl_file_transfer_get_instance_private (self); + g_autoptr(GTask) task = NULL; + gboolean ret; + + DZL_ENTRY; + + g_return_val_if_fail (DZL_IS_FILE_TRANSFER (self), FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + task = g_task_new (self, cancellable, NULL, NULL); + g_task_set_source_tag (task, dzl_file_transfer_execute); + + if (priv->executed) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVAL, + "Transfer can only be executed once."); + DZL_RETURN (FALSE); + } + + g_task_set_check_cancellable (task, TRUE); + g_task_set_return_on_cancel (task, TRUE); + g_task_set_priority (task, io_priority); + g_task_set_task_data (task, g_steal_pointer (&priv->opers), (GDestroyNotify)g_ptr_array_unref); + g_task_run_in_thread_sync (task, dzl_file_transfer_worker); + + ret = g_task_propagate_boolean (task, error); + + DZL_RETURN (ret); +} + +void +dzl_file_transfer_execute_async (DzlFileTransfer *self, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + DzlFileTransferPrivate *priv = dzl_file_transfer_get_instance_private (self); + g_autoptr(GTask) task = NULL; + + DZL_ENTRY; + + g_return_if_fail (DZL_IS_FILE_TRANSFER (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, NULL, NULL); + g_task_set_source_tag (task, dzl_file_transfer_execute); + + if (priv->executed) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVAL, + "Transfer can only be executed once."); + DZL_EXIT; + } + + priv->executed = TRUE; + + g_task_set_check_cancellable (task, TRUE); + g_task_set_return_on_cancel (task, TRUE); + g_task_set_priority (task, io_priority); + g_task_set_task_data (task, g_steal_pointer (&priv->opers), (GDestroyNotify)g_ptr_array_unref); + g_task_run_in_thread (task, dzl_file_transfer_worker); + + DZL_EXIT; +} + +gboolean +dzl_file_transfer_execute_finish (DzlFileTransfer *self, + GAsyncResult *result, + GError **error) +{ + gboolean ret; + + DZL_ENTRY; + + g_return_val_if_fail (DZL_IS_FILE_TRANSFER (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_task_is_valid (G_TASK (result), self), FALSE); + + ret = g_task_propagate_boolean (G_TASK (result), error); + + DZL_RETURN (ret); +} + +/** + * dzl_file_transfer_stat: + * @self: a #DzlFileTransfer + * @stat_buf: (out): a #DzlFileTransferStat + * + * Gets statistics about the transfer progress. + * + * Since: 3.28 + */ +void +dzl_file_transfer_stat (DzlFileTransfer *self, + DzlFileTransferStat *stat_buf) +{ + DzlFileTransferPrivate *priv = dzl_file_transfer_get_instance_private (self); + + g_return_if_fail (DZL_IS_FILE_TRANSFER (self)); + g_return_if_fail (stat_buf != NULL); + + *stat_buf = priv->stat_buf; +} diff --git a/src/files/dzl-file-transfer.h b/src/files/dzl-file-transfer.h new file mode 100644 index 0000000..007d18b --- /dev/null +++ b/src/files/dzl-file-transfer.h @@ -0,0 +1,91 @@ +/* dzl-file-transfer.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#pragma once + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_FILE_TRANSFER (dzl_file_transfer_get_type()) + +DZL_AVAILABLE_IN_3_28 +G_DECLARE_DERIVABLE_TYPE (DzlFileTransfer, dzl_file_transfer, DZL, FILE_TRANSFER, GObject) + +struct _DzlFileTransferClass +{ + GObjectClass parent_class; + + /*< private >*/ + gpointer _padding[12]; +}; + +typedef enum +{ + DZL_FILE_TRANSFER_FLAGS_NONE = 0, + DZL_FILE_TRANSFER_FLAGS_MOVE = 1 << 0, +} DzlFileTransferFlags; + +typedef struct +{ + gint64 n_files_total; + gint64 n_files; + gint64 n_dirs_total; + gint64 n_dirs; + gint64 n_bytes_total; + gint64 n_bytes; + + /*< private >*/ + gint64 _padding[10]; +} DzlFileTransferStat; + +DZL_AVAILABLE_IN_3_28 +DzlFileTransfer *dzl_file_transfer_new (void); +DZL_AVAILABLE_IN_3_28 +DzlFileTransferFlags dzl_file_transfer_get_flags (DzlFileTransfer *self); +DZL_AVAILABLE_IN_3_28 +void dzl_file_transfer_set_flags (DzlFileTransfer *self, + DzlFileTransferFlags flags); +DZL_AVAILABLE_IN_3_28 +gdouble dzl_file_transfer_get_progress (DzlFileTransfer *self); +DZL_AVAILABLE_IN_3_28 +void dzl_file_transfer_stat (DzlFileTransfer *self, + DzlFileTransferStat *stat_buf); +DZL_AVAILABLE_IN_3_28 +void dzl_file_transfer_add (DzlFileTransfer *self, + GFile *src, + GFile *dest); +DZL_AVAILABLE_IN_3_28 +void dzl_file_transfer_execute_async (DzlFileTransfer *self, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +DZL_AVAILABLE_IN_3_28 +gboolean dzl_file_transfer_execute_finish (DzlFileTransfer *self, + GAsyncResult *result, + GError **error); +DZL_AVAILABLE_IN_3_28 +gboolean dzl_file_transfer_execute (DzlFileTransfer *self, + gint io_priority, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/src/files/dzl-recursive-file-monitor.c b/src/files/dzl-recursive-file-monitor.c new file mode 100644 index 0000000..092699d --- /dev/null +++ b/src/files/dzl-recursive-file-monitor.c @@ -0,0 +1,665 @@ +/* dzl-recursive-file-monitor.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-recursive-file-monitor" + +#include "config.h" + +#include +#include + +#include "files/dzl-recursive-file-monitor.h" +#include "util/dzl-macros.h" + +#define MONITOR_FLAGS 0 + +/** + * SECTION:dzl-recursive-file-monitor + * @title: DzlRecursiveFileMonitor + * @short_description: a recursive directory monitor + * + * This works by creating a #GFileMonitor for each directory underneath a root + * directory (and recursively beyond that). + * + * This is only designed for use on Linux, where we are using a single inotify + * FD. You can still hit the max watch limit, but it is much higher than the FD + * limit. + * + * Since: 3.28 + */ + +struct _DzlRecursiveFileMonitor +{ + GObject parent_instance; + + GFile *root; + GCancellable *cancellable; + + GHashTable *monitors_by_file; + GHashTable *files_by_monitor; + + DzlRecursiveIgnoreFunc ignore_func; + gpointer ignore_func_data; + GDestroyNotify ignore_func_data_destroy; +}; + +enum { + PROP_0, + PROP_ROOT, + N_PROPS +}; + +enum { + CHANGED, + N_SIGNALS +}; + +G_DEFINE_TYPE (DzlRecursiveFileMonitor, dzl_recursive_file_monitor, G_TYPE_OBJECT) + +static GParamSpec *properties [N_PROPS]; +static guint signals [N_SIGNALS]; + +static void +dzl_recursive_file_monitor_track (DzlRecursiveFileMonitor *self, + GFile *dir, + GFileMonitor *monitor); + +static void +dzl_recursive_file_monitor_unwatch (DzlRecursiveFileMonitor *self, + GFile *file) +{ + GFileMonitor *monitor; + + dzl_assert_is_main_thread (); + g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); + g_assert (G_IS_FILE (file)); + + monitor = g_hash_table_lookup (self->monitors_by_file, file); + + if (monitor != NULL) + { + g_object_ref (monitor); + g_file_monitor_cancel (monitor); + g_hash_table_remove (self->monitors_by_file, file); + g_hash_table_remove (self->files_by_monitor, monitor); + g_object_unref (monitor); + } +} + +static void +dzl_recursive_file_monitor_collect_recursive (GPtrArray *dirs, + GFile *parent, + GCancellable *cancellable) +{ + g_autoptr(GFileEnumerator) enumerator = NULL; + g_autoptr(GError) error = NULL; + + g_assert (dirs != NULL); + g_assert (G_IS_FILE (parent)); + g_assert (G_IS_CANCELLABLE (cancellable)); + + enumerator = g_file_enumerate_children (parent, + G_FILE_ATTRIBUTE_STANDARD_NAME"," + G_FILE_ATTRIBUTE_STANDARD_TYPE, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable, &error); + + if (error != NULL) + { + g_warning ("Failed to iterate children: %s", error->message); + g_clear_error (&error); + } + + if (enumerator != NULL) + { + gpointer infoptr; + + while (NULL != (infoptr = g_file_enumerator_next_file (enumerator, cancellable, NULL))) + { + g_autoptr(GFileInfo) info = infoptr; + + if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) + { + const gchar *name = g_file_info_get_name (info); + g_autoptr(GFile) child = g_file_get_child (parent, name); + + /* + * We add the child, and then recurse into the child immediately + * so that we can keep the invariant that all descendants + * immediately follow their ancestor. This allows us to simplify + * our ignored-directory checks when we get back to the main + * thread. + */ + + g_ptr_array_add (dirs, g_object_ref (child)); + dzl_recursive_file_monitor_collect_recursive (dirs, child, cancellable); + } + } + + g_file_enumerator_close (enumerator, cancellable, NULL); + g_clear_object (&enumerator); + } +} + +static GFile * +resolve_file (GFile *file) +{ + g_autofree gchar *orig_path = NULL; + g_autoptr(GFile) new_file = NULL; + char *real_path; + + g_assert (G_IS_FILE (file)); + + /* + * The goal here is to work our way up to the root and resolve any + * symlinks in the path. If the file is not native, we don't care + * about symlinks. + */ + if (!g_file_is_native (file)) + return g_object_ref (file); + + orig_path = g_file_get_path (file); + real_path = realpath (orig_path, NULL); + + /* unlikely, but PATH_MAX exceeded */ + if (real_path == NULL) + return g_object_ref (file); + + new_file = g_file_new_for_path (real_path); + free (real_path); + + return g_steal_pointer (&new_file); +} + +static void +dzl_recursive_file_monitor_collect_worker (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr(GPtrArray) dirs = NULL; + g_autoptr(GFile) resolved = NULL; + GFile *root = task_data; + + g_assert (G_IS_TASK (task)); + g_assert (G_IS_FILE (root)); + + /* The first thing we want to do is resolve any symlinks out of + * the path so that we are consistently working with the real + * system path. This improves interaction with other APIs that + * might not have given the callee back the symlink'd path and + * instead the real path. + */ + resolved = resolve_file (root); + + dirs = g_ptr_array_new_with_free_func (g_object_unref); + g_ptr_array_add (dirs, g_object_ref (resolved)); + dzl_recursive_file_monitor_collect_recursive (dirs, resolved, cancellable); + + g_task_return_pointer (task, + g_steal_pointer (&dirs), + (GDestroyNotify)g_ptr_array_unref); +} + +static void +dzl_recursive_file_monitor_collect (DzlRecursiveFileMonitor *self, + GFile *root, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); + g_assert (G_IS_FILE (root)); + g_assert (G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, dzl_recursive_file_monitor_collect); + g_task_set_priority (task, G_PRIORITY_LOW); + g_task_set_task_data (task, g_object_ref (root), g_object_unref); + g_task_run_in_thread (task, dzl_recursive_file_monitor_collect_worker); +} + +static GPtrArray * +dzl_recursive_file_monitor_collect_finish (DzlRecursiveFileMonitor *self, + GAsyncResult *result, + GError **error) +{ + g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); + g_assert (G_IS_TASK (result)); + g_assert (g_task_is_valid (G_TASK (result), self)); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static gboolean +dzl_recursive_file_monitor_ignored (DzlRecursiveFileMonitor *self, + GFile *file) +{ + dzl_assert_is_main_thread (); + g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); + g_assert (G_IS_FILE (file)); + + if (self->ignore_func != NULL) + return self->ignore_func (file, self->ignore_func_data); + + return FALSE; +} + +static void +dzl_recursive_file_monitor_changed (DzlRecursiveFileMonitor *self, + GFile *file, + GFile *other_file, + GFileMonitorEvent event, + GFileMonitor *monitor) +{ + dzl_assert_is_main_thread (); + g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); + g_assert (G_IS_FILE (file)); + g_assert (!other_file || G_IS_FILE (file)); + g_assert (G_IS_FILE_MONITOR (monitor)); + + if (g_cancellable_is_cancelled (self->cancellable)) + return; + + if (dzl_recursive_file_monitor_ignored (self, file)) + return; + + if (event == G_FILE_MONITOR_EVENT_DELETED) + { + if (g_hash_table_contains (self->monitors_by_file, file)) + dzl_recursive_file_monitor_unwatch (self, file); + } + else if (event == G_FILE_MONITOR_EVENT_CREATED) + { + if (g_file_query_file_type (file, 0, NULL) == G_FILE_TYPE_DIRECTORY) + { + g_autoptr(GPtrArray) dirs = NULL; + + dirs = g_ptr_array_new_with_free_func (g_object_unref); + g_ptr_array_add (dirs, g_object_ref (file)); + + dzl_recursive_file_monitor_collect_recursive (dirs, file, self->cancellable); + + for (guint i = 0; i < dirs->len; i++) + { + g_autoptr(GFileMonitor) dir_monitor = NULL; + GFile *dir = g_ptr_array_index (dirs, i); + + if (!!(dir_monitor = g_file_monitor_directory (dir, MONITOR_FLAGS, self->cancellable, NULL))) + dzl_recursive_file_monitor_track (self, dir, dir_monitor); + } + } + } + + g_signal_emit (self, signals [CHANGED], 0, file, other_file, event); +} + + +static void +dzl_recursive_file_monitor_track (DzlRecursiveFileMonitor *self, + GFile *dir, + GFileMonitor *monitor) +{ + dzl_assert_is_main_thread (); + g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); + g_assert (G_IS_FILE (dir)); + g_assert (G_IS_FILE_MONITOR (monitor)); + + g_hash_table_insert (self->monitors_by_file, + g_object_ref (dir), + g_object_ref (monitor)); + + g_hash_table_insert (self->files_by_monitor, + g_object_ref (monitor), + g_object_ref (dir)); + + g_signal_connect_object (monitor, + "changed", + G_CALLBACK (dzl_recursive_file_monitor_changed), + self, + G_CONNECT_SWAPPED); +} + +static void +dzl_recursive_file_monitor_start_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + DzlRecursiveFileMonitor *self = (DzlRecursiveFileMonitor *)object; + g_autoptr(GPtrArray) dirs = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GTask) task = user_data; + + dzl_assert_is_main_thread (); + g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (G_IS_TASK (task)); + + dirs = dzl_recursive_file_monitor_collect_finish (self, result, &error); + + if (dirs == NULL) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + for (guint i = 0; i < dirs->len; i++) + { + GFile *dir = g_ptr_array_index (dirs, i); + g_autoptr(GFileMonitor) monitor = NULL; + + g_assert (G_IS_FILE (dir)); + + if (dzl_recursive_file_monitor_ignored (self, dir)) + { + /* + * Skip ahead to the next directory that does not have this directory + * as a prefix. We can do this because we know the descendants are + * guaranteed to immediately follow this directory. + */ + + for (guint j = i + 1; j < dirs->len; j++, i++) + { + GFile *next = g_ptr_array_index (dirs, j); + + if (!g_file_has_prefix (next, dir)) + break; + } + + continue; + } + + monitor = g_file_monitor_directory (dir, MONITOR_FLAGS, self->cancellable, &error); + + if (monitor == NULL) + { + g_warning ("Failed to monitor directory: %s", error->message); + g_clear_error (&error); + continue; + } + + dzl_recursive_file_monitor_track (self, dir, monitor); + } + + g_task_return_boolean (task, TRUE); +} + +void +dzl_recursive_file_monitor_start_async (DzlRecursiveFileMonitor *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, dzl_recursive_file_monitor_start_async); + g_task_set_return_on_cancel (task, TRUE); + g_task_set_task_data (task, g_object_ref (self->root), g_object_unref); + g_task_set_priority (task, G_PRIORITY_LOW); + + if (self->root == NULL) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVAL, + "Cannot start file monitor, no root directory set"); + return; + } + + dzl_recursive_file_monitor_collect (self, + self->root, + self->cancellable, + dzl_recursive_file_monitor_start_cb, + g_steal_pointer (&task)); +} + +gboolean +dzl_recursive_file_monitor_start_finish (DzlRecursiveFileMonitor *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_task_is_valid (G_TASK (result), self), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +dzl_recursive_file_monitor_constructed (GObject *object) +{ + DzlRecursiveFileMonitor *self = (DzlRecursiveFileMonitor *)object; + + G_OBJECT_CLASS (dzl_recursive_file_monitor_parent_class)->constructed (object); + + if (self->root == NULL) + g_warning ("%s created without a root directory", G_OBJECT_TYPE_NAME (self)); +} + +static void +dzl_recursive_file_monitor_dispose (GObject *object) +{ + DzlRecursiveFileMonitor *self = (DzlRecursiveFileMonitor *)object; + + g_cancellable_cancel (self->cancellable); + dzl_recursive_file_monitor_set_ignore_func (self, NULL, NULL, NULL); + + G_OBJECT_CLASS (dzl_recursive_file_monitor_parent_class)->dispose (object); +} + +static void +dzl_recursive_file_monitor_finalize (GObject *object) +{ + DzlRecursiveFileMonitor *self = (DzlRecursiveFileMonitor *)object; + + g_clear_object (&self->root); + g_clear_object (&self->cancellable); + + g_clear_pointer (&self->files_by_monitor, g_hash_table_unref); + g_clear_pointer (&self->monitors_by_file, g_hash_table_unref); + + G_OBJECT_CLASS (dzl_recursive_file_monitor_parent_class)->finalize (object); +} + +static void +dzl_recursive_file_monitor_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlRecursiveFileMonitor *self = DZL_RECURSIVE_FILE_MONITOR (object); + + switch (prop_id) + { + case PROP_ROOT: + g_value_set_object (value, dzl_recursive_file_monitor_get_root (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_recursive_file_monitor_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlRecursiveFileMonitor *self = DZL_RECURSIVE_FILE_MONITOR (object); + + switch (prop_id) + { + case PROP_ROOT: + self->root = g_value_dup_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_recursive_file_monitor_class_init (DzlRecursiveFileMonitorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = dzl_recursive_file_monitor_constructed; + object_class->dispose = dzl_recursive_file_monitor_dispose; + object_class->finalize = dzl_recursive_file_monitor_finalize; + object_class->get_property = dzl_recursive_file_monitor_get_property; + object_class->set_property = dzl_recursive_file_monitor_set_property; + + properties [PROP_ROOT] = + g_param_spec_object ("root", + "Root", + "The root directory to monitor", + G_TYPE_FILE, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + /** + * DzlRecursiveFileMonitor::changed: + * @self: a #DzlRecursiveFileMonitor + * @file: a #GFile + * @other_file: (nullable): a #GFile for the other file when applicable + * @event: the #GFileMonitorEvent event + * + * This event is similar to #GFileMonitor::changed but can be fired from + * any of the monitored directories in the recursive mount. + * + * Since: 3.28 + */ + signals [CHANGED] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 3, G_TYPE_FILE, G_TYPE_FILE, G_TYPE_FILE_MONITOR_EVENT); +} + +static void +dzl_recursive_file_monitor_init (DzlRecursiveFileMonitor *self) +{ + self->cancellable = g_cancellable_new (); + self->files_by_monitor = g_hash_table_new_full (NULL, NULL, g_object_unref, g_object_unref); + self->monitors_by_file = g_hash_table_new_full (g_file_hash, + (GEqualFunc) g_file_equal, + g_object_unref, + g_object_unref); +} + +DzlRecursiveFileMonitor * +dzl_recursive_file_monitor_new (GFile *file) +{ + g_return_val_if_fail (G_IS_FILE (file), NULL); + + return g_object_new (DZL_TYPE_RECURSIVE_FILE_MONITOR, + "root", file, + NULL); +} + +/** + * dzl_recursive_file_monitor_cancel: + * @self: a #DzlRecursiveFileMonitor + * + * Cancels the recursive file monitor. + * + * Since: 3.28 + */ +void +dzl_recursive_file_monitor_cancel (DzlRecursiveFileMonitor *self) +{ + g_return_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self)); + + g_object_run_dispose (G_OBJECT (self)); +} + +/** + * dzl_recursive_file_monitor_get_root: + * @self: a #DzlRecursiveFileMonitor + * + * Gets the root directory used forthe file monitor. + * + * Returns: (transfer none): a #GFile + * + * Since: 3.28 + */ +GFile * +dzl_recursive_file_monitor_get_root (DzlRecursiveFileMonitor *self) +{ + g_return_val_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self), NULL); + + return self->root; +} + +/** + * dzl_recursive_file_monitor_set_ignore_func: + * @self: a #DzlRecursiveFileMonitor + * @ignore_func: (scope async): a #DzlRecursiveIgnoreFunc + * @ignore_func_data: closure data for @ignore_func + * @ignore_func_data_destroy: destroy notify for @ignore_func_data + * + * Sets a callback function to determine if a #GFile should be ignored + * from signal emission. + * + * @ignore_func will always be called from the applications main thread. + * + * If @ignore_func is %NULL, it is set to the default which does not + * ignore any files or directories. + * + * Since: 3.28 + */ +void +dzl_recursive_file_monitor_set_ignore_func (DzlRecursiveFileMonitor *self, + DzlRecursiveIgnoreFunc ignore_func, + gpointer ignore_func_data, + GDestroyNotify ignore_func_data_destroy) +{ + dzl_assert_is_main_thread (); + g_return_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self)); + + if (ignore_func == NULL) + { + ignore_func_data = NULL; + ignore_func_data_destroy = NULL; + } + + if (self->ignore_func_data && self->ignore_func_data_destroy) + { + gpointer data = self->ignore_func_data; + GDestroyNotify notify = self->ignore_func_data_destroy; + + self->ignore_func = NULL; + self->ignore_func_data = NULL; + self->ignore_func_data_destroy = NULL; + + notify (data); + } + + self->ignore_func = ignore_func; + self->ignore_func_data = ignore_func_data; + self->ignore_func_data_destroy = ignore_func_data_destroy; +} diff --git a/src/files/dzl-recursive-file-monitor.h b/src/files/dzl-recursive-file-monitor.h new file mode 100644 index 0000000..077bdaf --- /dev/null +++ b/src/files/dzl-recursive-file-monitor.h @@ -0,0 +1,59 @@ +/* dzl-recursive-file-monitor.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_RECURSIVE_FILE_MONITOR_H +#define DZL_RECURSIVE_FILE_MONITOR_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_RECURSIVE_FILE_MONITOR (dzl_recursive_file_monitor_get_type()) + +DZL_AVAILABLE_IN_3_28 +G_DECLARE_FINAL_TYPE (DzlRecursiveFileMonitor, dzl_recursive_file_monitor, DZL, RECURSIVE_FILE_MONITOR, GObject) + +typedef gboolean (*DzlRecursiveIgnoreFunc) (GFile *file, + gpointer user_data); + +DZL_AVAILABLE_IN_3_28 +DzlRecursiveFileMonitor *dzl_recursive_file_monitor_new (GFile *root); +DZL_AVAILABLE_IN_3_28 +GFile *dzl_recursive_file_monitor_get_root (DzlRecursiveFileMonitor *self); +DZL_AVAILABLE_IN_3_28 +void dzl_recursive_file_monitor_start_async (DzlRecursiveFileMonitor *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +DZL_AVAILABLE_IN_3_28 +gboolean dzl_recursive_file_monitor_start_finish (DzlRecursiveFileMonitor *self, + GAsyncResult *result, + GError **error); +DZL_AVAILABLE_IN_3_28 +void dzl_recursive_file_monitor_cancel (DzlRecursiveFileMonitor *self); +DZL_AVAILABLE_IN_3_28 +void dzl_recursive_file_monitor_set_ignore_func (DzlRecursiveFileMonitor *self, + DzlRecursiveIgnoreFunc ignore_func, + gpointer ignore_func_data, + GDestroyNotify ignore_func_data_destroy); + +G_END_DECLS + +#endif /* DZL_RECURSIVE_FILE_MONITOR_H */ diff --git a/src/files/meson.build b/src/files/meson.build new file mode 100644 index 0000000..49d7b0d --- /dev/null +++ b/src/files/meson.build @@ -0,0 +1,23 @@ +files_headers = [ + 'dzl-directory-model.h', + 'dzl-directory-reaper.h', + 'dzl-file-transfer.h', + 'dzl-recursive-file-monitor.h', +] + +files_sources = [ + 'dzl-directory-model.c', + 'dzl-directory-reaper.c', + 'dzl-file-transfer.c', + 'dzl-recursive-file-monitor.c', +] + +files_enums_headers = [ + 'dzl-file-transfer.h', +] + +libdazzle_public_headers += files(files_headers) +libdazzle_public_sources += files(files_sources) +dzl_enum_headers += files(files_enums_headers) + +install_headers(files_headers, subdir: join_paths(libdazzle_header_subdir, 'files')) diff --git a/src/graphing/dzl-cpu-graph.c b/src/graphing/dzl-cpu-graph.c new file mode 100644 index 0000000..9d07713 --- /dev/null +++ b/src/graphing/dzl-cpu-graph.c @@ -0,0 +1,186 @@ +/* dzl-cpu-graph.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#include + +#include "config.h" + +#include "dzl-cpu-graph.h" +#include "dzl-cpu-model.h" +#include "dzl-graph-line-renderer.h" + +struct _DzlCpuGraph +{ + DzlGraphView parent_instance; + + gint64 timespan; + guint max_samples; +}; + +G_DEFINE_TYPE (DzlCpuGraph, dzl_cpu_graph, DZL_TYPE_GRAPH_VIEW) + +enum { + PROP_0, + PROP_MAX_SAMPLES, + PROP_TIMESPAN, + LAST_PROP +}; + +static GParamSpec *properties[LAST_PROP]; + +static gchar *colors[] = { + "#73d216", + "#f57900", + "#3465a4", + "#ef2929", + "#75507b", + "#ce5c00", + "#c17d11", + "#cc0000", +}; + +GtkWidget * +dzl_cpu_graph_new (void) +{ + return g_object_new (DZL_TYPE_CPU_GRAPH, NULL); +} + +static void +dzl_cpu_graph_constructed (GObject *object) +{ + static DzlCpuModel *model; + DzlCpuGraph *self = (DzlCpuGraph *)object; + guint n_columns; + guint i; + + G_OBJECT_CLASS (dzl_cpu_graph_parent_class)->constructed (object); + + /* + * Create a model, but allow it to be destroyed after the last + * graph releases it. We will recreate it on demand. + */ + if (model == NULL) + { + model = g_object_new (DZL_TYPE_CPU_MODEL, + "timespan", self->timespan, + "max-samples", self->max_samples + 1, + NULL); + g_object_add_weak_pointer (G_OBJECT (model), (gpointer *)&model); + dzl_graph_view_set_model (DZL_GRAPH_VIEW (self), DZL_GRAPH_MODEL (model)); + g_object_unref (model); + } + else + { + dzl_graph_view_set_model (DZL_GRAPH_VIEW (self), DZL_GRAPH_MODEL (model)); + } + + n_columns = dzl_graph_view_model_get_n_columns (DZL_GRAPH_MODEL (model)); + + for (i = 0; i < n_columns; i++) + { + DzlGraphRenderer *renderer; + + renderer = g_object_new (DZL_TYPE_GRAPH_LINE_RENDERER, + "column", i, + "stroke-color", colors [i % G_N_ELEMENTS (colors)], + NULL); + dzl_graph_view_add_renderer (DZL_GRAPH_VIEW (self), renderer); + g_clear_object (&renderer); + } +} + +static void +dzl_cpu_graph_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlCpuGraph *self = DZL_CPU_GRAPH (object); + + switch (prop_id) + { + case PROP_MAX_SAMPLES: + g_value_set_uint (value, self->max_samples); + break; + + case PROP_TIMESPAN: + g_value_set_int64 (value, self->timespan); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_cpu_graph_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlCpuGraph *self = DZL_CPU_GRAPH (object); + + switch (prop_id) + { + case PROP_MAX_SAMPLES: + self->max_samples = g_value_get_uint (value); + break; + + case PROP_TIMESPAN: + self->timespan = g_value_get_int64 (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_cpu_graph_class_init (DzlCpuGraphClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = dzl_cpu_graph_constructed; + object_class->get_property = dzl_cpu_graph_get_property; + object_class->set_property = dzl_cpu_graph_set_property; + + properties [PROP_TIMESPAN] = + g_param_spec_int64 ("timespan", + "Timespan", + "Timespan", + 0, G_MAXINT64, + 0, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_MAX_SAMPLES] = + g_param_spec_uint ("max-samples", + "Max Samples", + "Max Samples", + 0, G_MAXUINT, + 120, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_cpu_graph_init (DzlCpuGraph *self) +{ + self->max_samples = 120; + self->timespan = 60L * G_USEC_PER_SEC; +} diff --git a/src/graphing/dzl-cpu-graph.h b/src/graphing/dzl-cpu-graph.h new file mode 100644 index 0000000..f2d3e83 --- /dev/null +++ b/src/graphing/dzl-cpu-graph.h @@ -0,0 +1,38 @@ +/* dzl-cpu-graph.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_CPU_GRAPH_H +#define DZL_CPU_GRAPH_H + +#include "dzl-version-macros.h" + +#include "dzl-graph-view.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_CPU_GRAPH (dzl_cpu_graph_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlCpuGraph, dzl_cpu_graph, DZL, CPU_GRAPH, DzlGraphView) + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_cpu_graph_new (void); + +G_END_DECLS + +#endif /* DZL_CPU_GRAPH_H */ diff --git a/src/graphing/dzl-cpu-model.c b/src/graphing/dzl-cpu-model.c new file mode 100644 index 0000000..e3af205 --- /dev/null +++ b/src/graphing/dzl-cpu-model.c @@ -0,0 +1,373 @@ +/* dzl-cpu-model.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#if defined(__FreeBSD__) +# include +# include +# include +# include +#endif + +#include "graphing/dzl-cpu-model.h" +#include "util/dzl-macros.h" + +#ifdef __linux__ +# define PROC_STAT_BUF_SIZE 4096 +#endif + +typedef struct +{ + gdouble total; + gdouble freq; + glong last_user; + glong last_idle; + glong last_system; + glong last_nice; + glong last_iowait; + glong last_irq; + glong last_softirq; + glong last_steal; + glong last_guest; + glong last_guest_nice; +} CpuInfo; + +struct _DzlCpuModel +{ + DzlGraphModel parent_instance; + + GArray *cpu_info; + guint n_cpu; + +#ifdef __linux__ + gint stat_fd; + gchar *stat_buf; +#endif + + guint poll_source; + guint poll_interval_msec; +}; + +G_DEFINE_TYPE (DzlCpuModel, dzl_cpu_model, DZL_TYPE_GRAPH_MODEL) + +#ifdef __linux__ +static gboolean +read_stat (DzlCpuModel *self) +{ + gssize len; + + g_assert (self != NULL); + g_assert (self->stat_fd != -1); + g_assert (self->stat_buf != NULL); + + if (lseek (self->stat_fd, 0, SEEK_SET) != 0) + return FALSE; + + len = read (self->stat_fd, self->stat_buf, PROC_STAT_BUF_SIZE); + if (len <= 0) + return FALSE; + + if (len < PROC_STAT_BUF_SIZE) + self->stat_buf[len] = 0; + else + self->stat_buf[PROC_STAT_BUF_SIZE-1] = 0; + + return TRUE; +} + +static void +dzl_cpu_model_poll (DzlCpuModel *self) +{ + gchar cpu[64] = { 0 }; + glong user; + glong sys; + glong nice; + glong idle; + glong iowait; + glong irq; + glong softirq; + glong steal; + glong guest; + glong guest_nice; + glong user_calc; + glong system_calc; + glong nice_calc; + glong idle_calc; + glong iowait_calc; + glong irq_calc; + glong softirq_calc; + glong steal_calc; + glong guest_calc; + glong guest_nice_calc; + glong total; + gchar *line; + gint ret; + gint id; + + if (read_stat (self)) + { + line = self->stat_buf; + + for (gsize i = 0; self->stat_buf[i]; i++) + { + if (self->stat_buf[i] == '\n') { + self->stat_buf[i] = '\0'; + + if (strncmp (line, "cpu", 3) == 0) + { + if (isdigit (line[3])) + { + CpuInfo *cpu_info; + + user = nice = sys = idle = id = 0; + ret = sscanf (line, "%s %ld %ld %ld %ld %ld %ld %ld %ld %ld %ld", + cpu, &user, &nice, &sys, &idle, + &iowait, &irq, &softirq, &steal, &guest, &guest_nice); + if (ret != 11) + goto next; + + ret = sscanf(cpu, "cpu%d", &id); + + if (ret != 1 || id < 0 || id >= (gint)self->n_cpu) + goto next; + + cpu_info = &g_array_index (self->cpu_info, CpuInfo, id); + + user_calc = user - cpu_info->last_user; + nice_calc = nice - cpu_info->last_nice; + system_calc = sys - cpu_info->last_system; + idle_calc = idle - cpu_info->last_idle; + iowait_calc = iowait - cpu_info->last_iowait; + irq_calc = irq - cpu_info->last_irq; + softirq_calc = softirq - cpu_info->last_softirq; + steal_calc = steal - cpu_info->last_steal; + guest_calc = guest - cpu_info->last_guest; + guest_nice_calc = guest_nice - cpu_info->last_guest_nice; + + total = user_calc + nice_calc + system_calc + idle_calc + iowait_calc + irq_calc + softirq_calc + steal_calc + guest_calc + guest_nice_calc; + cpu_info->total = ((total - idle_calc) / (gdouble)total) * 100.0; + + cpu_info->last_user = user; + cpu_info->last_nice = nice; + cpu_info->last_idle = idle; + cpu_info->last_system = sys; + cpu_info->last_iowait = iowait; + cpu_info->last_irq = irq; + cpu_info->last_softirq = softirq; + cpu_info->last_steal = steal; + cpu_info->last_guest = guest; + cpu_info->last_guest_nice = guest_nice; + } + } else { + /* CPU info comes first. Skip further lines. */ + break; + } + + next: + line = &self->stat_buf[i + 1]; + } + } + } +} + +#elif defined(__FreeBSD__) + +static void +dzl_cpu_model_poll (DzlCpuModel *self) +{ + static gint mib_cp_times[2]; + static gsize len_cp_times = 2; + + if (mib_cp_times[0] == 0 || mib_cp_times[1] == 0) + { + if (sysctlnametomib ("kern.cp_times", mib_cp_times, &len_cp_times) == -1) + { + g_critical ("Cannot convert sysctl name kern.cp_times to a mib array: %s", + g_strerror (errno)); + return; + } + } + + gsize cp_times_size = sizeof (glong) * CPUSTATES * self->n_cpu; + glong *cp_times = g_malloc (cp_times_size); + + if (sysctl (mib_cp_times, 2, cp_times, &cp_times_size, NULL, 0) == -1) + { + g_critical ("Cannot get CPU usage by sysctl kern.cp_times: %s", + g_strerror (errno)); + g_free (cp_times); + return; + } + + for (guint i = 0, j = 0; i < self->n_cpu; i++, j += CPUSTATES) + { + CpuInfo *cpu_info = &g_array_index (self->cpu_info, CpuInfo, i); + + glong user = cp_times[j + CP_USER]; + glong nice = cp_times[j + CP_NICE]; + glong sys = cp_times[j + CP_SYS]; + glong irq = cp_times[j + CP_INTR]; + glong idle = cp_times[j + CP_IDLE]; + + glong user_calc = user - cpu_info->last_user; + glong nice_calc = nice - cpu_info->last_nice; + glong system_calc = sys - cpu_info->last_system; + glong irq_calc = irq - cpu_info->last_irq; + glong idle_calc = idle - cpu_info->last_idle; + + glong total = user_calc + nice_calc + system_calc + irq_calc + idle_calc; + cpu_info->total = ((total - idle_calc) / (gdouble)total) * 100.0; + + cpu_info->last_user = user; + cpu_info->last_nice = nice; + cpu_info->last_system = sys; + cpu_info->last_irq = irq; + cpu_info->last_idle = idle; + } + g_free (cp_times); +} +#else +static void +dzl_cpu_model_poll (DzlCpuModel *self) +{ + /* + * TODO: calculate cpu info for OpenBSD/etc. + * + * While we are at it, we should make the Linux code above non-shitty. + */ +} +#endif + +static gboolean +dzl_cpu_model_poll_cb (gpointer user_data) +{ + DzlCpuModel *self = user_data; + DzlGraphModelIter iter; + guint i; + + dzl_cpu_model_poll (self); + + dzl_graph_view_model_push (DZL_GRAPH_MODEL (self), &iter, g_get_monotonic_time ()); + + for (i = 0; i < self->cpu_info->len; i++) + { + CpuInfo *cpu_info; + + cpu_info = &g_array_index (self->cpu_info, CpuInfo, i); + dzl_graph_view_model_iter_set (&iter, i, cpu_info->total, -1); + } + + return G_SOURCE_CONTINUE; +} + +static void +dzl_cpu_model_constructed (GObject *object) +{ + DzlCpuModel *self = (DzlCpuModel *)object; + gint64 timespan; + guint max_samples; + + G_OBJECT_CLASS (dzl_cpu_model_parent_class)->constructed (object); + + max_samples = dzl_graph_view_model_get_max_samples (DZL_GRAPH_MODEL (self)); + timespan = dzl_graph_view_model_get_timespan (DZL_GRAPH_MODEL (self)); + + self->poll_interval_msec = (gdouble)timespan / (gdouble)(max_samples - 1) / 1000L; + + if (self->poll_interval_msec == 0) + { + g_critical ("Implausible timespan/max_samples combination for graph."); + self->poll_interval_msec = 1000; + } + + self->n_cpu = g_get_num_processors (); + + for (guint i = 0; i < self->n_cpu; i++) + { + CpuInfo cpu_info = { 0 }; + DzlGraphColumn *column; + gchar *name; + + name = g_strdup_printf ("CPU %d", i + 1); + column = dzl_graph_view_column_new (name, G_TYPE_DOUBLE); + + dzl_graph_view_model_add_column (DZL_GRAPH_MODEL (self), column); + g_array_append_val (self->cpu_info, cpu_info); + + g_object_unref (column); + g_free (name); + } + + dzl_cpu_model_poll (self); + + self->poll_source = g_timeout_add (self->poll_interval_msec, dzl_cpu_model_poll_cb, self); +} + +static void +dzl_cpu_model_finalize (GObject *object) +{ + DzlCpuModel *self = (DzlCpuModel *)object; + +#ifdef __linux__ + g_clear_pointer (&self->stat_buf, g_free); + if (self->stat_fd != -1) + close (self->stat_fd); +#endif + + dzl_clear_source (&self->poll_source); + g_clear_pointer (&self->cpu_info, g_array_unref); + + G_OBJECT_CLASS (dzl_cpu_model_parent_class)->finalize (object); +} + +static void +dzl_cpu_model_class_init (DzlCpuModelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = dzl_cpu_model_constructed; + object_class->finalize = dzl_cpu_model_finalize; +} + +static void +dzl_cpu_model_init (DzlCpuModel *self) +{ + self->cpu_info = g_array_new (FALSE, FALSE, sizeof (CpuInfo)); + +#ifdef __linux__ + self->stat_fd = open ("/proc/stat", O_RDONLY); + self->stat_buf = g_malloc (PROC_STAT_BUF_SIZE); +#endif + + g_object_set (self, + "value-min", 0.0, + "value-max", 100.0, + NULL); +} + +DzlGraphModel * +dzl_cpu_model_new (void) +{ + return g_object_new (DZL_TYPE_CPU_MODEL, NULL); +} diff --git a/src/graphing/dzl-cpu-model.h b/src/graphing/dzl-cpu-model.h new file mode 100644 index 0000000..fa55206 --- /dev/null +++ b/src/graphing/dzl-cpu-model.h @@ -0,0 +1,38 @@ +/* dzl-cpu-model.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_CPU_MODEL_H +#define DZL_CPU_MODEL_H + +#include "dzl-version-macros.h" + +#include "dzl-graph-model.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_CPU_MODEL (dzl_cpu_model_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlCpuModel, dzl_cpu_model, DZL, CPU_MODEL, DzlGraphModel) + +DZL_AVAILABLE_IN_ALL +DzlGraphModel *dzl_cpu_model_new (void); + +G_END_DECLS + +#endif /* DZL_CPU_MODEL_H */ diff --git a/src/graphing/dzl-graph-column-private.h b/src/graphing/dzl-graph-column-private.h new file mode 100644 index 0000000..2b222cf --- /dev/null +++ b/src/graphing/dzl-graph-column-private.h @@ -0,0 +1,49 @@ +/* dzl-graph-column-private.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_GRAPH_COLUMN_PRIVATE_H +#define DZL_GRAPH_COLUMN_PRIVATE_H + +#include + +#include "dzl-graph-column.h" + +G_BEGIN_DECLS + +void _dzl_graph_view_column_get_value (DzlGraphColumn *self, + guint index, + GValue *value); +void _dzl_graph_view_column_collect (DzlGraphColumn *self, + guint index, + va_list args); +void _dzl_graph_view_column_lcopy (DzlGraphColumn *self, + guint index, + va_list args); +void _dzl_graph_view_column_get (DzlGraphColumn *column, + guint index, + ...); +void _dzl_graph_view_column_set (DzlGraphColumn *column, + guint index, + ...); +guint _dzl_graph_view_column_push (DzlGraphColumn *column); +void _dzl_graph_view_column_set_n_rows (DzlGraphColumn *column, + guint n_rows); + +G_END_DECLS + +#endif /* DZL_GRAPH_COLUMN_PRIVATE_H */ diff --git a/src/graphing/dzl-graph-column.c b/src/graphing/dzl-graph-column.c new file mode 100644 index 0000000..653a61d --- /dev/null +++ b/src/graphing/dzl-graph-column.c @@ -0,0 +1,307 @@ +/* dzl-graph-column.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#include "config.h" + +#include +#include + +#include "dzl-graph-column.h" +#include "dzl-graph-column-private.h" +#include "util/dzl-ring.h" + +struct _DzlGraphColumn +{ + GObject parent_instance; + gchar *name; + DzlRing *values; + GType value_type; +}; + +G_DEFINE_TYPE (DzlGraphColumn, dzl_graph_view_column, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_NAME, + PROP_VALUE_TYPE, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +DzlGraphColumn * +dzl_graph_view_column_new (const gchar *name, + GType value_type) +{ + return g_object_new (DZL_TYPE_GRAPH_COLUMN, + "name", name, + "value-type", value_type, + NULL); +} + +const gchar * +dzl_graph_view_column_get_name (DzlGraphColumn *self) +{ + g_return_val_if_fail (DZL_IS_GRAPH_COLUMN (self), NULL); + + return self->name; +} + +void +dzl_graph_view_column_set_name (DzlGraphColumn *self, + const gchar *name) +{ + g_return_if_fail (DZL_IS_GRAPH_COLUMN (self)); + + if (g_strcmp0 (name, self->name) != 0) + { + g_free (self->name); + self->name = g_strdup (name); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NAME]); + } +} + +static void +dzl_graph_view_column_copy_value (gpointer data, + gpointer user_data) +{ + const GValue *src_value = data; + DzlRing *ring = user_data; + GValue copy = G_VALUE_INIT; + + if (G_IS_VALUE (src_value)) + { + g_value_init (©, G_VALUE_TYPE (src_value)); + g_value_copy (src_value, ©); + } + + dzl_ring_append_val (ring, copy); +} + +void +_dzl_graph_view_column_set_n_rows (DzlGraphColumn *self, + guint n_rows) +{ + DzlRing *ring; + + g_return_if_fail (DZL_IS_GRAPH_COLUMN (self)); + g_return_if_fail (n_rows > 0); + + ring = dzl_ring_sized_new (sizeof (GValue), n_rows, NULL); + dzl_ring_foreach (self->values, dzl_graph_view_column_copy_value, ring); + g_clear_pointer (&self->values, dzl_ring_unref); + self->values = ring; +} + +guint +_dzl_graph_view_column_push (DzlGraphColumn *self) +{ + GValue value = G_VALUE_INIT; + guint ret; + + g_return_val_if_fail (DZL_IS_GRAPH_COLUMN (self), 0); + + g_value_init (&value, self->value_type); + ret = dzl_ring_append_val (self->values, value); + + return ret; +} + +void +_dzl_graph_view_column_get_value (DzlGraphColumn *self, + guint index, + GValue *value) +{ + const GValue *src_value; + + g_return_if_fail (DZL_IS_GRAPH_COLUMN (self)); + g_return_if_fail (value != NULL); + g_return_if_fail (index < self->values->len); + + src_value = &((GValue *)(gpointer)self->values->data)[index]; + + g_value_init (value, self->value_type); + if (G_IS_VALUE (src_value)) + g_value_copy (src_value, value); +} + +void +_dzl_graph_view_column_collect (DzlGraphColumn *self, + guint index, + va_list args) +{ + GValue *value; + gchar *errmsg = NULL; + + g_return_if_fail (DZL_IS_GRAPH_COLUMN (self)); + g_return_if_fail (index < self->values->len); + + value = &((GValue *)(gpointer)self->values->data)[index]; + + G_VALUE_COLLECT (value, args, 0, &errmsg); + + if (G_UNLIKELY (errmsg != NULL)) + { + g_critical ("%s", errmsg); + g_free (errmsg); + } +} + +void +_dzl_graph_view_column_set (DzlGraphColumn *self, + guint index, + ...) +{ + va_list args; + + g_return_if_fail (DZL_IS_GRAPH_COLUMN (self)); + g_return_if_fail (index < self->values->len); + + va_start (args, index); + _dzl_graph_view_column_collect (self, index, args); + va_end (args); +} + +void +_dzl_graph_view_column_get (DzlGraphColumn *self, + guint index, + ...) +{ + va_list args; + + g_return_if_fail (DZL_IS_GRAPH_COLUMN (self)); + g_return_if_fail (index < self->values->len); + + va_start (args, index); + _dzl_graph_view_column_lcopy (self, index, args); + va_end (args); +} + +void +_dzl_graph_view_column_lcopy (DzlGraphColumn *self, + guint index, + va_list args) +{ + const GValue *value; + gchar *errmsg = NULL; + + g_return_if_fail (DZL_IS_GRAPH_COLUMN (self)); + g_return_if_fail (index < self->values->len); + + value = &((GValue *)(gpointer)self->values->data)[index]; + + if (!G_IS_VALUE (value)) + return; + + G_VALUE_LCOPY (value, args, 0, &errmsg); + + if (G_UNLIKELY (errmsg != NULL)) + { + g_critical ("%s", errmsg); + g_free (errmsg); + } +} + +static void +dzl_graph_view_column_finalize (GObject *object) +{ + DzlGraphColumn *self = (DzlGraphColumn *)object; + + g_clear_pointer (&self->name, g_free); + g_clear_pointer (&self->values, dzl_ring_unref); + + G_OBJECT_CLASS (dzl_graph_view_column_parent_class)->finalize (object); +} + +static void +dzl_graph_view_column_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlGraphColumn *self = DZL_GRAPH_COLUMN (object); + + switch (prop_id) + { + case PROP_NAME: + g_value_set_string (value, dzl_graph_view_column_get_name (self)); + break; + + case PROP_VALUE_TYPE: + g_value_set_gtype (value, self->value_type); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_graph_view_column_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlGraphColumn *self = DZL_GRAPH_COLUMN (object); + + switch (prop_id) + { + case PROP_NAME: + dzl_graph_view_column_set_name (self, g_value_get_string (value)); + break; + + case PROP_VALUE_TYPE: + self->value_type = g_value_get_gtype (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_graph_view_column_class_init (DzlGraphColumnClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_graph_view_column_finalize; + object_class->get_property = dzl_graph_view_column_get_property; + object_class->set_property = dzl_graph_view_column_set_property; + + properties [PROP_NAME] = + g_param_spec_string ("name", + "Name", + "The name of the column", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_VALUE_TYPE] = + g_param_spec_gtype ("value-type", + "Value Type", + "Value Type", + G_TYPE_NONE, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_graph_view_column_init (DzlGraphColumn *self) +{ + self->values = dzl_ring_sized_new (sizeof (GValue), 60, (GDestroyNotify)g_value_unset); +} diff --git a/src/graphing/dzl-graph-column.h b/src/graphing/dzl-graph-column.h new file mode 100644 index 0000000..e16266b --- /dev/null +++ b/src/graphing/dzl-graph-column.h @@ -0,0 +1,49 @@ +/* dzl-graph-column.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_GRAPH_COLUMN_H +#define DZL_GRAPH_COLUMN_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_GRAPH_COLUMN (dzl_graph_view_column_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlGraphColumn, dzl_graph_view_column, DZL, GRAPH_COLUMN, GObject) + +struct _DzlGraphColumnClass +{ + GObjectClass parent; +}; + +DZL_AVAILABLE_IN_ALL +DzlGraphColumn *dzl_graph_view_column_new (const gchar *name, + GType value_type); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_graph_view_column_get_name (DzlGraphColumn *self); +DZL_AVAILABLE_IN_ALL +void dzl_graph_view_column_set_name (DzlGraphColumn *self, + const gchar *name); + +G_END_DECLS + +#endif /* DZL_GRAPH_COLUMN_H */ diff --git a/src/graphing/dzl-graph-line-renderer.c b/src/graphing/dzl-graph-line-renderer.c new file mode 100644 index 0000000..3da778b --- /dev/null +++ b/src/graphing/dzl-graph-line-renderer.c @@ -0,0 +1,326 @@ +/* dzl-graph-line-renderer.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#include "config.h" + +#include +#include + +#include "dzl-graph-line-renderer.h" + +struct _DzlGraphLineRenderer +{ + GObject parent_instance; + + GdkRGBA stroke_color; + gdouble line_width; + guint column; +}; + +static void dzl_graph_view_line_renderer_init_renderer (DzlGraphRendererInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (DzlGraphLineRenderer, dzl_graph_view_line_renderer, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (DZL_TYPE_GRAPH_RENDERER, + dzl_graph_view_line_renderer_init_renderer)) + +enum { + PROP_0, + PROP_COLUMN, + PROP_LINE_WIDTH, + PROP_STROKE_COLOR, + PROP_STROKE_COLOR_RGBA, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +DzlGraphLineRenderer * +dzl_graph_view_line_renderer_new (void) +{ + return g_object_new (DZL_TYPE_GRAPH_LINE_RENDERER, NULL); +} + +static gdouble +calc_x (DzlGraphModelIter *iter, + gint64 begin, + gint64 end, + guint width) +{ + gint64 timestamp; + + timestamp = dzl_graph_view_model_iter_get_timestamp (iter); + + g_assert_cmpint (timestamp, !=, 0); + + return ((timestamp - begin) / (gdouble)(end - begin) * width); +} + +static gdouble +calc_y (DzlGraphModelIter *iter, + gdouble range_begin, + gdouble range_end, + guint height, + guint column) +{ + GValue value = G_VALUE_INIT; + gdouble y; + + dzl_graph_view_model_iter_get_value (iter, column, &value); + + switch (G_VALUE_TYPE (&value)) + { + case G_TYPE_DOUBLE: + y = g_value_get_double (&value); + break; + + case G_TYPE_UINT: + y = g_value_get_uint (&value); + break; + + case G_TYPE_UINT64: + y = g_value_get_uint64 (&value); + break; + + case G_TYPE_INT: + y = g_value_get_int (&value); + break; + + case G_TYPE_INT64: + y = g_value_get_int64 (&value); + break; + + default: + y = 0.0; + break; + } + + y -= range_begin; + y /= (range_end - range_begin); + y = height - (y * height); + + return y; +} + +static void +dzl_graph_view_line_renderer_render (DzlGraphRenderer *renderer, + DzlGraphModel *table, + gint64 x_begin, + gint64 x_end, + gdouble y_begin, + gdouble y_end, + cairo_t *cr, + const cairo_rectangle_int_t *area) +{ + DzlGraphLineRenderer *self = (DzlGraphLineRenderer *)renderer; + DzlGraphModelIter iter; + + g_assert (DZL_IS_GRAPH_LINE_RENDERER (self)); + + cairo_save (cr); + + if (dzl_graph_view_model_get_iter_first (table, &iter)) + { + guint max_samples; + gdouble chunk; + gdouble last_x; + gdouble last_y; + + max_samples = dzl_graph_view_model_get_max_samples (table); + + chunk = area->width / (gdouble)(max_samples - 1) / 2.0; + + last_x = calc_x (&iter, x_begin, x_end, area->width); + last_y = calc_y (&iter, y_begin, y_end, area->height, self->column); + + cairo_move_to (cr, last_x, last_y); + + while (dzl_graph_view_model_iter_next (&iter)) + { + gdouble x; + gdouble y; + + x = calc_x (&iter, x_begin, x_end, area->width); + y = calc_y (&iter, y_begin, y_end, area->height, self->column); + + cairo_curve_to (cr, + last_x + chunk, + last_y, + last_x + chunk, + y, + x, + y); + + last_x = x; + last_y = y; + } + } + + cairo_set_line_width (cr, self->line_width); + gdk_cairo_set_source_rgba (cr, &self->stroke_color); + cairo_stroke (cr); + + cairo_restore (cr); +} + +static void +dzl_graph_view_line_renderer_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlGraphLineRenderer *self = DZL_GRAPH_LINE_RENDERER (object); + + switch (prop_id) + { + case PROP_COLUMN: + g_value_set_uint (value, self->column); + break; + + case PROP_LINE_WIDTH: + g_value_set_double (value, self->line_width); + break; + + case PROP_STROKE_COLOR: + g_value_take_string (value, gdk_rgba_to_string (&self->stroke_color)); + break; + + case PROP_STROKE_COLOR_RGBA: + g_value_set_boxed (value, &self->stroke_color); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_graph_view_line_renderer_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlGraphLineRenderer *self = DZL_GRAPH_LINE_RENDERER (object); + + switch (prop_id) + { + case PROP_COLUMN: + self->column = g_value_get_uint (value); + break; + + case PROP_LINE_WIDTH: + self->line_width = g_value_get_double (value); + break; + + case PROP_STROKE_COLOR: + dzl_graph_view_line_renderer_set_stroke_color (self, g_value_get_string (value)); + break; + + case PROP_STROKE_COLOR_RGBA: + dzl_graph_view_line_renderer_set_stroke_color_rgba (self, g_value_get_boxed (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_graph_view_line_renderer_class_init (DzlGraphLineRendererClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = dzl_graph_view_line_renderer_get_property; + object_class->set_property = dzl_graph_view_line_renderer_set_property; + + properties [PROP_COLUMN] = + g_param_spec_uint ("column", + "Column", + "Column", + 0, G_MAXUINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_LINE_WIDTH] = + g_param_spec_double ("line-width", + "Line Width", + "Line Width", + 0.0, G_MAXDOUBLE, + 1.0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_STROKE_COLOR] = + g_param_spec_string ("stroke-color", + "Stroke Color", + "Stroke Color", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_STROKE_COLOR_RGBA] = + g_param_spec_boxed ("stroke-color-rgba", + "Stroke Color RGBA", + "Stroke Color RGBA", + GDK_TYPE_RGBA, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_graph_view_line_renderer_init (DzlGraphLineRenderer *self) +{ + self->line_width = 1.0; +} + +static void +dzl_graph_view_line_renderer_init_renderer (DzlGraphRendererInterface *iface) +{ + iface->render = dzl_graph_view_line_renderer_render; +} + +void +dzl_graph_view_line_renderer_set_stroke_color_rgba (DzlGraphLineRenderer *self, + const GdkRGBA *rgba) +{ + const GdkRGBA black = { 0, 0, 0, 1.0 }; + + g_return_if_fail (DZL_IS_GRAPH_LINE_RENDERER (self)); + + if (rgba == NULL) + rgba = &black; + + if (!gdk_rgba_equal (rgba, &self->stroke_color)) + { + self->stroke_color = *rgba; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STROKE_COLOR_RGBA]); + } +} + +void +dzl_graph_view_line_renderer_set_stroke_color (DzlGraphLineRenderer *self, + const gchar *stroke_color) +{ + GdkRGBA rgba; + + g_return_if_fail (DZL_IS_GRAPH_LINE_RENDERER (self)); + + if (stroke_color == NULL) + stroke_color = "#000000"; + + if (gdk_rgba_parse (&rgba, stroke_color)) + dzl_graph_view_line_renderer_set_stroke_color_rgba (self, &rgba); +} diff --git a/src/graphing/dzl-graph-line-renderer.h b/src/graphing/dzl-graph-line-renderer.h new file mode 100644 index 0000000..23d0618 --- /dev/null +++ b/src/graphing/dzl-graph-line-renderer.h @@ -0,0 +1,48 @@ +/* dzl-graph-line-renderer.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_GRAPH_LINE_RENDERER_H +#define DZL_GRAPH_LINE_RENDERER_H + +#include + +#include "dzl-version-macros.h" + +#include "dzl-graph-renderer.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_GRAPH_LINE_RENDERER (dzl_graph_view_line_renderer_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlGraphLineRenderer, dzl_graph_view_line_renderer, DZL, GRAPH_LINE_RENDERER, GObject) + +DZL_AVAILABLE_IN_ALL +DzlGraphLineRenderer *dzl_graph_view_line_renderer_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_graph_view_line_renderer_set_stroke_color (DzlGraphLineRenderer *self, + const gchar *stroke_color); +DZL_AVAILABLE_IN_ALL +void dzl_graph_view_line_renderer_set_stroke_color_rgba (DzlGraphLineRenderer *self, + const GdkRGBA *stroke_color_rgba); +DZL_AVAILABLE_IN_ALL +const GdkRGBA *dzl_graph_view_line_renderer_get_stroke_color_rgba (DzlGraphLineRenderer *self); + +G_END_DECLS + +#endif /* DZL_GRAPH_LINE_RENDERER_H */ diff --git a/src/graphing/dzl-graph-model.c b/src/graphing/dzl-graph-model.c new file mode 100644 index 0000000..2a74c7b --- /dev/null +++ b/src/graphing/dzl-graph-model.c @@ -0,0 +1,580 @@ +/* dzl-graph-model.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#include "config.h" + +#include + +#include "dzl-graph-column-private.h" +#include "dzl-graph-model.h" + +typedef struct +{ + GPtrArray *columns; + DzlGraphColumn *timestamps; + + guint last_index; + + guint max_samples; + GTimeSpan timespan; + gdouble value_max; + gdouble value_min; +} DzlGraphModelPrivate; + +typedef struct +{ + DzlGraphModel *table; + gint64 timestamp; + guint index; +} DzlGraphModelIterImpl; + +enum { + PROP_0, + PROP_MAX_SAMPLES, + PROP_TIMESPAN, + PROP_VALUE_MAX, + PROP_VALUE_MIN, + LAST_PROP +}; + +enum { + CHANGED, + LAST_SIGNAL +}; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlGraphModel, dzl_graph_view_model, G_TYPE_OBJECT) + +static GParamSpec *properties [LAST_PROP]; +static guint signals [LAST_SIGNAL]; + +gint64 +dzl_graph_view_model_get_timespan (DzlGraphModel *self) +{ + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_GRAPH_MODEL (self), 0); + + return priv->timespan; +} + +void +dzl_graph_view_model_set_timespan (DzlGraphModel *self, + GTimeSpan timespan) +{ + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + + g_return_if_fail (DZL_IS_GRAPH_MODEL (self)); + + if (timespan != priv->timespan) + { + priv->timespan = timespan; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TIMESPAN]); + } +} + +static void +dzl_graph_view_model_set_value_max (DzlGraphModel *self, + gdouble value_max) +{ + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + + g_return_if_fail (DZL_IS_GRAPH_MODEL (self)); + + if (priv->value_max != value_max) + { + priv->value_max = value_max; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VALUE_MAX]); + } +} + +static void +dzl_graph_view_model_set_value_min (DzlGraphModel *self, + gdouble value_min) +{ + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + + g_return_if_fail (DZL_IS_GRAPH_MODEL (self)); + + if (priv->value_min != value_min) + { + priv->value_min = value_min; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VALUE_MIN]); + } +} + +DzlGraphModel * +dzl_graph_view_model_new (void) +{ + return g_object_new (DZL_TYPE_GRAPH_MODEL, NULL); +} + +guint +dzl_graph_view_model_add_column (DzlGraphModel *self, + DzlGraphColumn *column) +{ + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_GRAPH_MODEL (self), 0); + g_return_val_if_fail (DZL_IS_GRAPH_COLUMN (column), 0); + + _dzl_graph_view_column_set_n_rows (column, priv->max_samples); + + g_ptr_array_add (priv->columns, g_object_ref (column)); + + return priv->columns->len - 1; +} + +guint +dzl_graph_view_model_get_n_columns (DzlGraphModel *self) +{ + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_GRAPH_MODEL (self), 0); + + return priv->columns->len; +} + +guint +dzl_graph_view_model_get_max_samples (DzlGraphModel *self) +{ + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_GRAPH_MODEL (self), 0); + + return priv->max_samples; +} + +void +dzl_graph_view_model_set_max_samples (DzlGraphModel *self, + guint max_samples) +{ + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + gsize i; + + g_return_if_fail (DZL_IS_GRAPH_MODEL (self)); + g_return_if_fail (max_samples > 0); + + if (max_samples == priv->max_samples) + return; + + for (i = 0; i < priv->columns->len; i++) + { + DzlGraphColumn *column; + + column = g_ptr_array_index (priv->columns, i); + _dzl_graph_view_column_set_n_rows (column, max_samples); + } + + _dzl_graph_view_column_set_n_rows (priv->timestamps, max_samples); + + priv->max_samples = max_samples; + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MAX_SAMPLES]); +} + +/** + * dzl_graph_view_model_push: + * @self: Table to push to + * @iter: (out): Newly created #DzlGraphModelIter + * @timestamp: Time of new event + */ +void +dzl_graph_view_model_push (DzlGraphModel *self, + DzlGraphModelIter *iter, + gint64 timestamp) +{ + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + DzlGraphModelIterImpl *impl = (DzlGraphModelIterImpl *)iter; + guint pos; + gsize i; + + g_return_if_fail (DZL_IS_GRAPH_MODEL (self)); + g_return_if_fail (iter != NULL); + g_return_if_fail (timestamp > 0); + + for (i = 0; i < priv->columns->len; i++) + { + DzlGraphColumn *column; + + column = g_ptr_array_index (priv->columns, i); + _dzl_graph_view_column_push (column); + } + + pos = _dzl_graph_view_column_push (priv->timestamps); + _dzl_graph_view_column_set (priv->timestamps, pos, timestamp); + + impl->table = self; + impl->timestamp = timestamp; + impl->index = pos; + + priv->last_index = pos; + + g_signal_emit (self, signals [CHANGED], 0); +} + +gboolean +dzl_graph_view_model_get_iter_last (DzlGraphModel *self, + DzlGraphModelIter *iter) +{ + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + DzlGraphModelIterImpl *impl = (DzlGraphModelIterImpl *)iter; + + g_return_val_if_fail (DZL_IS_GRAPH_MODEL (self), FALSE); + g_return_val_if_fail (iter != NULL, FALSE); + g_return_val_if_fail (impl != NULL, FALSE); + + impl->table = self; + impl->index = priv->last_index; + impl->timestamp = 0; + + _dzl_graph_view_column_get (priv->timestamps, impl->index, &impl->timestamp); + + return (impl->timestamp != 0); +} + +gint64 +dzl_graph_view_model_get_end_time (DzlGraphModel *self) +{ + DzlGraphModelIter iter; + + g_return_val_if_fail (DZL_IS_GRAPH_MODEL (self), 0); + + if (dzl_graph_view_model_get_iter_last (self, &iter)) + return dzl_graph_view_model_iter_get_timestamp (&iter); + + return g_get_monotonic_time (); +} + +gboolean +dzl_graph_view_model_get_iter_first (DzlGraphModel *self, + DzlGraphModelIter *iter) +{ + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + DzlGraphModelIterImpl *impl = (DzlGraphModelIterImpl *)iter; + + g_return_val_if_fail (DZL_IS_GRAPH_MODEL (self), FALSE); + g_return_val_if_fail (iter != NULL, FALSE); + g_return_val_if_fail (impl != NULL, FALSE); + + impl->table = self; + impl->index = (priv->last_index + 1) % priv->max_samples; + impl->timestamp = 0; + + _dzl_graph_view_column_get (priv->timestamps, impl->index, &impl->timestamp); + + /* + * Maybe this is our first time around the ring, and we can just + * assume the 0 index is the real first entry. + */ + if (impl->timestamp == 0) + { + impl->index = 0; + _dzl_graph_view_column_get (priv->timestamps, impl->index, &impl->timestamp); + } + + return (impl->timestamp != 0); +} + +gboolean +dzl_graph_view_model_iter_next (DzlGraphModelIter *iter) +{ + DzlGraphModelPrivate *priv; + DzlGraphModelIterImpl *impl = (DzlGraphModelIterImpl *)iter; + + g_return_val_if_fail (iter != NULL, FALSE); + g_return_val_if_fail (impl != NULL, FALSE); + g_return_val_if_fail (DZL_IS_GRAPH_MODEL (impl->table), FALSE); + + priv = dzl_graph_view_model_get_instance_private (impl->table); + + if (impl->index == priv->last_index) + { + impl->table = NULL; + impl->index = 0; + impl->timestamp = 0; + return FALSE; + } + + do + { + impl->index = (impl->index + 1) % priv->max_samples; + + impl->timestamp = 0; + _dzl_graph_view_column_get (priv->timestamps, impl->index, &impl->timestamp); + + if (impl->timestamp > 0) + break; + } + while (impl->index < priv->last_index); + + return (impl->timestamp > 0); +} + +gint64 +dzl_graph_view_model_iter_get_timestamp (DzlGraphModelIter *iter) +{ + DzlGraphModelIterImpl *impl = (DzlGraphModelIterImpl *)iter; + + g_return_val_if_fail (iter != NULL, 0); + + return impl->timestamp; +} + +void +dzl_graph_view_model_iter_set (DzlGraphModelIter *iter, + gint first_column, + ...) +{ + DzlGraphModelIterImpl *impl = (DzlGraphModelIterImpl *)iter; + DzlGraphModelPrivate *priv; + gint column_id = first_column; + va_list args; + + g_return_if_fail (iter != NULL); + g_return_if_fail (impl != NULL); + g_return_if_fail (DZL_IS_GRAPH_MODEL (impl->table)); + + priv = dzl_graph_view_model_get_instance_private (impl->table); + + va_start (args, first_column); + + while (column_id >= 0) + { + DzlGraphColumn *column; + + if (column_id >= (gint)priv->columns->len) + { + g_critical ("No such column %d", column_id); + goto cleanup; + } + + column = g_ptr_array_index (priv->columns, column_id); + + _dzl_graph_view_column_collect (column, impl->index, args); + + column_id = va_arg (args, gint); + } + + if (column_id != -1) + g_critical ("Invalid column sentinal: %d", column_id); + +cleanup: + va_end (args); +} + +void +dzl_graph_view_model_iter_get (DzlGraphModelIter *iter, + gint first_column, + ...) +{ + DzlGraphModelIterImpl *impl = (DzlGraphModelIterImpl *)iter; + DzlGraphModelPrivate *priv; + gint column_id = first_column; + va_list args; + + g_return_if_fail (iter != NULL); + g_return_if_fail (impl != NULL); + g_return_if_fail (DZL_IS_GRAPH_MODEL (impl->table)); + + priv = dzl_graph_view_model_get_instance_private (impl->table); + + va_start (args, first_column); + + while (column_id >= 0) + { + DzlGraphColumn *column; + + if (column_id >= (gint)priv->columns->len) + { + g_critical ("No such column %d", column_id); + goto cleanup; + } + + column = g_ptr_array_index (priv->columns, column_id); + + _dzl_graph_view_column_lcopy (column, impl->index, args); + + column_id = va_arg (args, gint); + } + + if (column_id != -1) + g_critical ("Invalid column sentinal: %d", column_id); + +cleanup: + va_end (args); +} + +void +dzl_graph_view_model_iter_get_value (DzlGraphModelIter *iter, + guint column, + GValue *value) +{ + DzlGraphModelIterImpl *impl = (DzlGraphModelIterImpl *)iter; + DzlGraphModelPrivate *priv; + DzlGraphColumn *col; + + g_return_if_fail (iter != NULL); + g_return_if_fail (impl != NULL); + g_return_if_fail (DZL_IS_GRAPH_MODEL (impl->table)); + priv = dzl_graph_view_model_get_instance_private (impl->table); + g_return_if_fail (column < priv->columns->len); + + col = g_ptr_array_index (priv->columns, column); + _dzl_graph_view_column_get_value (col, impl->index, value); +} + +static void +dzl_graph_view_model_finalize (GObject *object) +{ + DzlGraphModel *self = (DzlGraphModel *)object; + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + + g_clear_pointer (&priv->columns, g_ptr_array_unref); + + G_OBJECT_CLASS (dzl_graph_view_model_parent_class)->finalize (object); +} + +static void +dzl_graph_view_model_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlGraphModel *self = (DzlGraphModel *)object; + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + + switch (prop_id) + { + case PROP_TIMESPAN: + g_value_set_int64 (value, priv->timespan); + break; + + case PROP_MAX_SAMPLES: + g_value_set_uint (value, priv->max_samples); + break; + + case PROP_VALUE_MAX: + g_value_set_double (value, priv->value_max); + break; + + case PROP_VALUE_MIN: + g_value_set_double (value, priv->value_min); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_graph_view_model_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlGraphModel *self = (DzlGraphModel *)object; + + switch (prop_id) + { + case PROP_MAX_SAMPLES: + dzl_graph_view_model_set_max_samples (self, g_value_get_uint (value)); + break; + + case PROP_TIMESPAN: + dzl_graph_view_model_set_timespan (self, g_value_get_int64 (value)); + break; + + case PROP_VALUE_MAX: + dzl_graph_view_model_set_value_max (self, g_value_get_double (value)); + break; + + case PROP_VALUE_MIN: + dzl_graph_view_model_set_value_min (self, g_value_get_double (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_graph_view_model_class_init (DzlGraphModelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_graph_view_model_finalize; + object_class->get_property = dzl_graph_view_model_get_property; + object_class->set_property = dzl_graph_view_model_set_property; + + properties [PROP_MAX_SAMPLES] = + g_param_spec_uint ("max-samples", + "Max Samples", + "Max Samples", + 1, G_MAXUINT, + 120, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TIMESPAN] = + g_param_spec_int64 ("timespan", + "Timespan", + "Timespan to visualize, in microseconds.", + 1, G_MAXINT64, + G_USEC_PER_SEC * 60L, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); + + properties [PROP_VALUE_MAX] = + g_param_spec_double ("value-max", + "Value Max", + "Value Max", + -G_MINDOUBLE, G_MAXDOUBLE, + 100.0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_VALUE_MIN] = + g_param_spec_double ("value-min", + "Value Min", + "Value Min", + -G_MINDOUBLE, G_MAXDOUBLE, + 100.0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + signals [CHANGED] = g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); +} + +static void +dzl_graph_view_model_init (DzlGraphModel *self) +{ + DzlGraphModelPrivate *priv = dzl_graph_view_model_get_instance_private (self); + + priv->max_samples = 60; + priv->value_min = 0.0; + priv->value_max = 100.0; + + priv->columns = g_ptr_array_new_with_free_func (g_object_unref); + + priv->timestamps = dzl_graph_view_column_new (NULL, G_TYPE_INT64); + _dzl_graph_view_column_set_n_rows (priv->timestamps, priv->max_samples); +} diff --git a/src/graphing/dzl-graph-model.h b/src/graphing/dzl-graph-model.h new file mode 100644 index 0000000..5bde47a --- /dev/null +++ b/src/graphing/dzl-graph-model.h @@ -0,0 +1,93 @@ +/* dzl-graph-model.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_GRAPH_MODEL_H +#define DZL_GRAPH_MODEL_H + +#include + +#include "dzl-version-macros.h" + +#include "dzl-graph-column.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_GRAPH_MODEL (dzl_graph_view_model_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlGraphModel, dzl_graph_view_model, DZL, GRAPH_MODEL, GObject) + +struct _DzlGraphModelClass +{ + GObjectClass parent; +}; + +typedef struct +{ + gpointer data[8]; +} DzlGraphModelIter; + +DZL_AVAILABLE_IN_ALL +DzlGraphModel *dzl_graph_view_model_new (void); +DZL_AVAILABLE_IN_ALL +guint dzl_graph_view_model_add_column (DzlGraphModel *self, + DzlGraphColumn *column); +DZL_AVAILABLE_IN_ALL +guint dzl_graph_view_model_get_n_columns (DzlGraphModel *self); +DZL_AVAILABLE_IN_ALL +GTimeSpan dzl_graph_view_model_get_timespan (DzlGraphModel *self); +DZL_AVAILABLE_IN_ALL +void dzl_graph_view_model_set_timespan (DzlGraphModel *self, + GTimeSpan timespan); +DZL_AVAILABLE_IN_ALL +gint64 dzl_graph_view_model_get_end_time (DzlGraphModel *self); +DZL_AVAILABLE_IN_ALL +guint dzl_graph_view_model_get_max_samples (DzlGraphModel *self); +DZL_AVAILABLE_IN_ALL +void dzl_graph_view_model_set_max_samples (DzlGraphModel *self, + guint n_rows); +DZL_AVAILABLE_IN_ALL +void dzl_graph_view_model_push (DzlGraphModel *self, + DzlGraphModelIter *iter, + gint64 timestamp); +DZL_AVAILABLE_IN_ALL +gboolean dzl_graph_view_model_get_iter_first (DzlGraphModel *self, + DzlGraphModelIter *iter); +DZL_AVAILABLE_IN_ALL +gboolean dzl_graph_view_model_get_iter_last (DzlGraphModel *self, + DzlGraphModelIter *iter); +DZL_AVAILABLE_IN_ALL +gboolean dzl_graph_view_model_iter_next (DzlGraphModelIter *iter); +DZL_AVAILABLE_IN_ALL +void dzl_graph_view_model_iter_get (DzlGraphModelIter *iter, + gint first_column, + ...); +DZL_AVAILABLE_IN_ALL +void dzl_graph_view_model_iter_get_value (DzlGraphModelIter *iter, + guint column, + GValue *value); +DZL_AVAILABLE_IN_ALL +gint64 dzl_graph_view_model_iter_get_timestamp (DzlGraphModelIter *iter); +DZL_AVAILABLE_IN_ALL +void dzl_graph_view_model_iter_set (DzlGraphModelIter *iter, + gint first_column, + ...); + +G_END_DECLS + +#endif /* DZL_GRAPH_MODEL_H */ diff --git a/src/graphing/dzl-graph-renderer.c b/src/graphing/dzl-graph-renderer.c new file mode 100644 index 0000000..b705eab --- /dev/null +++ b/src/graphing/dzl-graph-renderer.c @@ -0,0 +1,60 @@ +/* dzl-graph-renderer.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#include "config.h" + +#include + +#include "dzl-graph-renderer.h" + +G_DEFINE_INTERFACE (DzlGraphRenderer, dzl_graph_view_renderer, G_TYPE_OBJECT) + +static void +dummy_render (DzlGraphRenderer *renderer, + DzlGraphModel *table, + gint64 x_begin, + gint64 x_end, + gdouble y_begin, + gdouble y_end, + cairo_t *cr, + const cairo_rectangle_int_t *area) +{ +} + +static void +dzl_graph_view_renderer_default_init (DzlGraphRendererInterface *iface) +{ + iface->render = dummy_render; +} + +void +dzl_graph_view_renderer_render (DzlGraphRenderer *self, + DzlGraphModel *table, + gint64 x_begin, + gint64 x_end, + gdouble y_begin, + gdouble y_end, + cairo_t *cr, + const cairo_rectangle_int_t *area) +{ + g_return_if_fail (DZL_IS_GRAPH_RENDERER (self)); + g_return_if_fail (cr != NULL); + g_return_if_fail (area != NULL); + + DZL_GRAPH_RENDERER_GET_IFACE (self)->render (self, table, x_begin, x_end, y_begin, y_end, cr, area); +} diff --git a/src/graphing/dzl-graph-renderer.h b/src/graphing/dzl-graph-renderer.h new file mode 100644 index 0000000..17c1a4c --- /dev/null +++ b/src/graphing/dzl-graph-renderer.h @@ -0,0 +1,60 @@ +/* dzl-graph-renderer.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_GRAPH_RENDERER_H +#define DZL_GRAPH_RENDERER_H + +#include + +#include "dzl-graph-model.h" +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_GRAPH_RENDERER (dzl_graph_view_renderer_get_type ()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_INTERFACE (DzlGraphRenderer, dzl_graph_view_renderer, DZL, GRAPH_RENDERER, GObject) + +struct _DzlGraphRendererInterface +{ + GTypeInterface parent; + + void (*render) (DzlGraphRenderer *self, + DzlGraphModel *table, + gint64 x_begin, + gint64 x_end, + gdouble y_begin, + gdouble y_end, + cairo_t *cr, + const cairo_rectangle_int_t *area); +}; + +DZL_AVAILABLE_IN_ALL +void dzl_graph_view_renderer_render (DzlGraphRenderer *self, + DzlGraphModel *table, + gint64 x_begin, + gint64 x_end, + gdouble y_begin, + gdouble y_end, + cairo_t *cr, + const cairo_rectangle_int_t *area); + +G_END_DECLS + +#endif /* DZL_GRAPH_RENDERER_H */ diff --git a/src/graphing/dzl-graph-view.c b/src/graphing/dzl-graph-view.c new file mode 100644 index 0000000..9920cea --- /dev/null +++ b/src/graphing/dzl-graph-view.c @@ -0,0 +1,450 @@ +/* dzl-graph-view.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#include "config.h" + +#include +#include + +#include "dzl-graph-view.h" + +typedef struct +{ + DzlGraphModel *model; + DzlSignalGroup *model_signals; + GPtrArray *renderers; + cairo_surface_t *surface; + guint tick_handler; + gdouble x_offset; + guint missed_count; + guint surface_dirty : 1; +} DzlGraphViewPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlGraphView, dzl_graph_view, GTK_TYPE_DRAWING_AREA) + +enum { + PROP_0, + PROP_TABLE, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +GtkWidget * +dzl_graph_view_new (void) +{ + return g_object_new (DZL_TYPE_GRAPH_VIEW, NULL); +} + +static void +dzl_graph_view_clear_surface (DzlGraphView *self) +{ + DzlGraphViewPrivate *priv = dzl_graph_view_get_instance_private (self); + + g_assert (DZL_IS_GRAPH_VIEW (self)); + + priv->surface_dirty = TRUE; +} + +/** + * dzl_graph_view_get_model: + * + * Gets the #DzlGraphView:model property. + * + * Returns: (transfer none) (nullable): An #DzlGraphModel or %NULL. + */ +DzlGraphModel * +dzl_graph_view_get_model (DzlGraphView *self) +{ + DzlGraphViewPrivate *priv = dzl_graph_view_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_GRAPH_VIEW (self), NULL); + + return priv->model; +} + +void +dzl_graph_view_set_model (DzlGraphView *self, + DzlGraphModel *model) +{ + DzlGraphViewPrivate *priv = dzl_graph_view_get_instance_private (self); + + g_return_if_fail (DZL_IS_GRAPH_VIEW (self)); + g_return_if_fail (!model || DZL_IS_GRAPH_MODEL (model)); + + if (g_set_object (&priv->model, model)) + { + dzl_signal_group_set_target (priv->model_signals, model); + gtk_widget_queue_allocate (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TABLE]); + } +} + +void +dzl_graph_view_add_renderer (DzlGraphView *self, + DzlGraphRenderer *renderer) +{ + DzlGraphViewPrivate *priv = dzl_graph_view_get_instance_private (self); + + g_return_if_fail (DZL_IS_GRAPH_VIEW (self)); + g_return_if_fail (DZL_IS_GRAPH_RENDERER (renderer)); + + g_ptr_array_add (priv->renderers, g_object_ref (renderer)); + dzl_graph_view_clear_surface (self); +} + +static gboolean +dzl_graph_view_tick_cb (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer user_data) +{ + DzlGraphView *self = (DzlGraphView *)widget; + DzlGraphViewPrivate *priv = dzl_graph_view_get_instance_private (self); + GtkAllocation alloc; + gint64 frame_time; + gint64 end_time; + gint64 timespan; + gdouble x_offset; + + g_assert (DZL_IS_GRAPH_VIEW (self)); + + if ((priv->surface == NULL) || (priv->model == NULL) || !gtk_widget_get_visible (widget)) + goto remove_handler; + + /* + * If we've missed drawings for the last 10 tick callbacks, chances are we're + * visible, but not being displayed to the user because we're not top-most. + * Disable ourselves in that case too so that we don't spin the frame-clock. + * + * We'll be re-enabled when the next ensure_surface() is called (upon a real + * draw by the system). + * + * The missed_count is reset on draw(). + */ + if (priv->missed_count > 10) + goto remove_handler; + else + priv->missed_count++; + + timespan = dzl_graph_view_model_get_timespan (priv->model); + if (timespan == 0) + goto remove_handler; + + gtk_widget_get_allocation (widget, &alloc); + + frame_time = g_get_monotonic_time (); + end_time = dzl_graph_view_model_get_end_time (priv->model); + + x_offset = -((frame_time - end_time) / (gdouble)timespan); + + if (x_offset != priv->x_offset) + { + priv->x_offset = x_offset; + gtk_widget_queue_draw (widget); + } + + return G_SOURCE_CONTINUE; + +remove_handler: + if (priv->tick_handler != 0) + { + gtk_widget_remove_tick_callback (widget, priv->tick_handler); + priv->tick_handler = 0; + } + + return G_SOURCE_REMOVE; +} + +static void +dzl_graph_view_ensure_surface (DzlGraphView *self) +{ + DzlGraphViewPrivate *priv = dzl_graph_view_get_instance_private (self); + GtkAllocation alloc; + DzlGraphModelIter iter; + gint64 begin_time; + gint64 end_time; + gdouble y_begin; + gdouble y_end; + cairo_t *cr; + gsize i; + + g_assert (DZL_IS_GRAPH_VIEW (self)); + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + if (priv->surface == NULL) + { + priv->surface_dirty = TRUE; + priv->surface = gdk_window_create_similar_surface (gtk_widget_get_window (GTK_WIDGET (self)), + CAIRO_CONTENT_COLOR_ALPHA, + alloc.width, + alloc.height); + } + + if (priv->model == NULL) + return; + + if (priv->surface_dirty) + { + priv->surface_dirty = FALSE; + + cr = cairo_create (priv->surface); + + cairo_save (cr); + cairo_rectangle (cr, 0, 0, alloc.width, alloc.height); + cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR); + cairo_fill (cr); + cairo_restore (cr); + + g_object_get (priv->model, + "value-min", &y_begin, + "value-max", &y_end, + NULL); + + dzl_graph_view_model_get_iter_last (priv->model, &iter); + end_time = dzl_graph_view_model_iter_get_timestamp (&iter); + begin_time = end_time - dzl_graph_view_model_get_timespan (priv->model); + + for (i = 0; i < priv->renderers->len; i++) + { + DzlGraphRenderer *renderer; + + renderer = g_ptr_array_index (priv->renderers, i); + + cairo_save (cr); + dzl_graph_view_renderer_render (renderer, priv->model, begin_time, end_time, y_begin, y_end, cr, &alloc); + cairo_restore (cr); + } + + cairo_destroy (cr); + } + + if (priv->tick_handler == 0) + priv->tick_handler = gtk_widget_add_tick_callback (GTK_WIDGET (self), + dzl_graph_view_tick_cb, + self, + NULL); +} + +static gboolean +dzl_graph_view_draw (GtkWidget *widget, + cairo_t *cr) +{ + DzlGraphView *self = (DzlGraphView *)widget; + DzlGraphViewPrivate *priv = dzl_graph_view_get_instance_private (self); + GtkStyleContext *style_context; + GtkAllocation alloc; + + g_assert (DZL_IS_GRAPH_VIEW (self)); + + priv->missed_count = 0; + + gtk_widget_get_allocation (widget, &alloc); + + style_context = gtk_widget_get_style_context (widget); + + dzl_graph_view_ensure_surface (self); + + gtk_style_context_save (style_context); + gtk_style_context_add_class (style_context, "view"); + gtk_render_background (style_context, cr, 0, 0, alloc.width, alloc.height); + gtk_style_context_restore (style_context); + + cairo_save (cr); + cairo_set_source_surface (cr, priv->surface, priv->x_offset * alloc.width, 0); + cairo_rectangle (cr, 0, 0, alloc.width, alloc.height); + cairo_fill (cr); + cairo_restore (cr); + + return GDK_EVENT_PROPAGATE; +} + +static void +dzl_graph_view_size_allocate (GtkWidget *widget, + GtkAllocation *alloc) +{ + DzlGraphView *self = (DzlGraphView *)widget; + DzlGraphViewPrivate *priv = dzl_graph_view_get_instance_private (self); + GtkAllocation old_alloc; + + g_assert (DZL_IS_GRAPH_VIEW (self)); + g_assert (alloc != NULL); + + gtk_widget_get_allocation (widget, &old_alloc); + + if ((old_alloc.width != alloc->width) || (old_alloc.height != alloc->height)) + g_clear_pointer (&priv->surface, cairo_surface_destroy); + + GTK_WIDGET_CLASS (dzl_graph_view_parent_class)->size_allocate (widget, alloc); +} + +static void +dzl_graph_view__model__changed (DzlGraphView *self, + DzlGraphModel *model) +{ + DzlGraphViewPrivate *priv = dzl_graph_view_get_instance_private (self); + + g_assert (DZL_IS_GRAPH_VIEW (self)); + g_assert (DZL_IS_GRAPH_MODEL (model)); + + priv->x_offset = 0; + + dzl_graph_view_clear_surface (self); +} + +static void +dzl_graph_view__model__notify_timespan (DzlGraphView *self, + GParamSpec *pspec, + DzlGraphModel *model) +{ + g_assert (DZL_IS_GRAPH_VIEW (self)); + g_assert (DZL_IS_GRAPH_MODEL (model)); + + /* Avoid this in a number of scenarios */ + if (gtk_widget_get_visible (GTK_WIDGET (self)) && + gtk_widget_get_child_visible (GTK_WIDGET (self))) + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + +static void +dzl_graph_view_destroy (GtkWidget *widget) +{ + DzlGraphView *self = (DzlGraphView *)widget; + DzlGraphViewPrivate *priv = dzl_graph_view_get_instance_private (self); + + if (priv->tick_handler != 0) + { + gtk_widget_remove_tick_callback (widget, priv->tick_handler); + priv->tick_handler = 0; + } + + g_clear_pointer (&priv->surface, cairo_surface_destroy); + + GTK_WIDGET_CLASS (dzl_graph_view_parent_class)->destroy (widget); +} + +static void +dzl_graph_view_finalize (GObject *object) +{ + DzlGraphView *self = (DzlGraphView *)object; + DzlGraphViewPrivate *priv = dzl_graph_view_get_instance_private (self); + + g_clear_object (&priv->model); + g_clear_object (&priv->model_signals); + g_clear_pointer (&priv->renderers, g_ptr_array_unref); + + G_OBJECT_CLASS (dzl_graph_view_parent_class)->finalize (object); +} + +static void +dzl_graph_view_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlGraphView *self = DZL_GRAPH_VIEW (object); + + switch (prop_id) + { + case PROP_TABLE: + g_value_set_object (value, dzl_graph_view_get_model (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_graph_view_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlGraphView *self = DZL_GRAPH_VIEW (object); + + switch (prop_id) + { + case PROP_TABLE: + dzl_graph_view_set_model (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_graph_view_class_init (DzlGraphViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_graph_view_finalize; + object_class->get_property = dzl_graph_view_get_property; + object_class->set_property = dzl_graph_view_set_property; + + widget_class->destroy = dzl_graph_view_destroy; + widget_class->draw = dzl_graph_view_draw; + widget_class->size_allocate = dzl_graph_view_size_allocate; + + properties [PROP_TABLE] = + g_param_spec_object ("model", + "Table", + "The data model for the graph.", + DZL_TYPE_GRAPH_MODEL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + gtk_widget_class_set_css_name (widget_class, "dzlgraphview"); +} + +static void +dzl_graph_view_init (DzlGraphView *self) +{ + DzlGraphViewPrivate *priv = dzl_graph_view_get_instance_private (self); + + priv->renderers = g_ptr_array_new_with_free_func (g_object_unref); + + priv->model_signals = dzl_signal_group_new (DZL_TYPE_GRAPH_MODEL); + + dzl_signal_group_connect_object (priv->model_signals, + "notify::value-max", + G_CALLBACK (gtk_widget_queue_allocate), + self, + G_CONNECT_SWAPPED); + + dzl_signal_group_connect_object (priv->model_signals, + "notify::value-min", + G_CALLBACK (gtk_widget_queue_allocate), + self, + G_CONNECT_SWAPPED); + + dzl_signal_group_connect_object (priv->model_signals, + "notify::timespan", + G_CALLBACK (dzl_graph_view__model__notify_timespan), + self, + G_CONNECT_SWAPPED); + + dzl_signal_group_connect_object (priv->model_signals, + "changed", + G_CALLBACK (dzl_graph_view__model__changed), + self, + G_CONNECT_SWAPPED); +} diff --git a/src/graphing/dzl-graph-view.h b/src/graphing/dzl-graph-view.h new file mode 100644 index 0000000..e95eea7 --- /dev/null +++ b/src/graphing/dzl-graph-view.h @@ -0,0 +1,63 @@ +/* dzl-graph-view.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_GRAPH_VIEW_H +#define DZL_GRAPH_VIEW_H + +#include + +#include "dzl-version-macros.h" + +#include "dzl-graph-model.h" +#include "dzl-graph-renderer.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_GRAPH_VIEW (dzl_graph_view_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlGraphView, dzl_graph_view, DZL, GRAPH_VIEW, GtkDrawingArea) + +struct _DzlGraphViewClass +{ + GtkDrawingAreaClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_graph_view_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_graph_view_set_model (DzlGraphView *self, + DzlGraphModel *model); +DZL_AVAILABLE_IN_ALL +DzlGraphModel *dzl_graph_view_get_model (DzlGraphView *self); +DZL_AVAILABLE_IN_ALL +void dzl_graph_view_add_renderer (DzlGraphView *self, + DzlGraphRenderer *renderer); + +G_END_DECLS + +#endif /* DZL_GRAPH_VIEW_H */ diff --git a/src/graphing/meson.build b/src/graphing/meson.build new file mode 100644 index 0000000..f968e94 --- /dev/null +++ b/src/graphing/meson.build @@ -0,0 +1,24 @@ +graphing_headers = [ + 'dzl-cpu-graph.h', + 'dzl-cpu-model.h', + 'dzl-graph-column.h', + 'dzl-graph-line-renderer.h', + 'dzl-graph-model.h', + 'dzl-graph-renderer.h', + 'dzl-graph-view.h', +] + +graphing_sources = [ + 'dzl-cpu-graph.c', + 'dzl-cpu-model.c', + 'dzl-graph-column.c', + 'dzl-graph-line-renderer.c', + 'dzl-graph-model.c', + 'dzl-graph-renderer.c', + 'dzl-graph-view.c', +] + +libdazzle_public_headers += files(graphing_headers) +libdazzle_public_sources += files(graphing_sources) + +install_headers(graphing_headers, subdir: join_paths(libdazzle_header_subdir, 'graphing')) diff --git a/src/menus/dzl-joined-menu.c b/src/menus/dzl-joined-menu.c new file mode 100644 index 0000000..5833291 --- /dev/null +++ b/src/menus/dzl-joined-menu.c @@ -0,0 +1,325 @@ +/* dzl-joined-menu.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-joined-menu" + +#include "config.h" + +#include "dzl-joined-menu.h" + +typedef struct +{ + GMenuModel *model; + gulong items_changed_handler; +} Menu; + +struct _DzlJoinedMenu +{ + GMenuModel parent_instance; + GArray *menus; +}; + +G_DEFINE_TYPE (DzlJoinedMenu, dzl_joined_menu, G_TYPE_MENU_MODEL) + +static void +clear_menu (gpointer data) +{ + Menu *menu = data; + + g_signal_handler_disconnect (menu->model, menu->items_changed_handler); + menu->items_changed_handler = 0; + g_clear_object (&menu->model); +} + +static gint +dzl_joined_menu_get_offset_at_index (DzlJoinedMenu *self, + gint index) +{ + gint offset = 0; + + for (guint i = 0; i < index; i++) + offset += g_menu_model_get_n_items (g_array_index (self->menus, Menu, i).model); + + return offset; +} + +static gint +dzl_joined_menu_get_offset_at_model (DzlJoinedMenu *self, + GMenuModel *model) +{ + gint offset = 0; + + for (guint i = 0; i < self->menus->len; i++) + { + const Menu *menu = &g_array_index (self->menus, Menu, i); + + if (menu->model == model) + break; + + offset += g_menu_model_get_n_items (menu->model); + } + + return offset; +} + +static gboolean +dzl_joined_menu_is_mutable (GMenuModel *model) +{ + return TRUE; +} + +static gint +dzl_joined_menu_get_n_items (GMenuModel *model) +{ + DzlJoinedMenu *self = (DzlJoinedMenu *)model; + + if (self->menus->len == 0) + return 0; + + return dzl_joined_menu_get_offset_at_index (self, self->menus->len); +} + +static const Menu * +dzl_joined_menu_get_item (DzlJoinedMenu *self, + gint *item_index) +{ + g_assert (DZL_IS_JOINED_MENU (self)); + + for (guint i = 0; i < self->menus->len; i++) + { + const Menu *menu = &g_array_index (self->menus, Menu, i); + gint n_items = g_menu_model_get_n_items (menu->model); + + if (n_items > *item_index) + return menu; + + (*item_index) -= n_items; + } + + g_assert_not_reached (); + + return NULL; +} + +static void +dzl_joined_menu_get_item_attributes (GMenuModel *model, + gint item_index, + GHashTable **attributes) +{ + const Menu *menu = dzl_joined_menu_get_item (DZL_JOINED_MENU (model), &item_index); + G_MENU_MODEL_GET_CLASS (menu->model)->get_item_attributes (menu->model, item_index, attributes); +} + +static GMenuAttributeIter * +dzl_joined_menu_iterate_item_attributes (GMenuModel *model, + gint item_index) +{ + const Menu *menu = dzl_joined_menu_get_item (DZL_JOINED_MENU (model), &item_index); + return G_MENU_MODEL_GET_CLASS (menu->model)->iterate_item_attributes (menu->model, item_index); +} + +static GVariant * +dzl_joined_menu_get_item_attribute_value (GMenuModel *model, + gint item_index, + const gchar *attribute, + const GVariantType *expected_type) +{ + const Menu *menu = dzl_joined_menu_get_item (DZL_JOINED_MENU (model), &item_index); + return G_MENU_MODEL_GET_CLASS (menu->model)->get_item_attribute_value (menu->model, item_index, attribute, expected_type); +} + +static void +dzl_joined_menu_get_item_links (GMenuModel *model, + gint item_index, + GHashTable **links) +{ + const Menu *menu = dzl_joined_menu_get_item (DZL_JOINED_MENU (model), &item_index); + G_MENU_MODEL_GET_CLASS (menu->model)->get_item_links (menu->model, item_index, links); +} + +static GMenuLinkIter * +dzl_joined_menu_iterate_item_links (GMenuModel *model, + gint item_index) +{ + const Menu *menu = dzl_joined_menu_get_item (DZL_JOINED_MENU (model), &item_index); + return G_MENU_MODEL_GET_CLASS (menu->model)->iterate_item_links (menu->model, item_index); +} + +static GMenuModel * +dzl_joined_menu_get_item_link (GMenuModel *model, + gint item_index, + const gchar *link) +{ + const Menu *menu = dzl_joined_menu_get_item (DZL_JOINED_MENU (model), &item_index); + return G_MENU_MODEL_GET_CLASS (menu->model)->get_item_link (menu->model, item_index, link); +} + +static void +dzl_joined_menu_finalize (GObject *object) +{ + DzlJoinedMenu *self = (DzlJoinedMenu *)object; + + g_clear_pointer (&self->menus, g_array_unref); + + G_OBJECT_CLASS (dzl_joined_menu_parent_class)->finalize (object); +} + +static void +dzl_joined_menu_class_init (DzlJoinedMenuClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GMenuModelClass *menu_model_class = G_MENU_MODEL_CLASS (klass); + + object_class->finalize = dzl_joined_menu_finalize; + + menu_model_class->is_mutable = dzl_joined_menu_is_mutable; + menu_model_class->get_n_items = dzl_joined_menu_get_n_items; + menu_model_class->get_item_attributes = dzl_joined_menu_get_item_attributes; + menu_model_class->iterate_item_attributes = dzl_joined_menu_iterate_item_attributes; + menu_model_class->get_item_attribute_value = dzl_joined_menu_get_item_attribute_value; + menu_model_class->get_item_links = dzl_joined_menu_get_item_links; + menu_model_class->iterate_item_links = dzl_joined_menu_iterate_item_links; + menu_model_class->get_item_link = dzl_joined_menu_get_item_link; +} + +static void +dzl_joined_menu_init (DzlJoinedMenu *self) +{ + self->menus = g_array_new (FALSE, FALSE, sizeof (Menu)); + g_array_set_clear_func (self->menus, clear_menu); +} + +static void +dzl_joined_menu_on_items_changed (DzlJoinedMenu *self, + guint offset, + guint removed, + guint added, + GMenuModel *model) +{ + g_assert (DZL_IS_JOINED_MENU (self)); + g_assert (G_IS_MENU_MODEL (model)); + + offset += dzl_joined_menu_get_offset_at_model (self, model); + g_menu_model_items_changed (G_MENU_MODEL (self), offset, removed, added); +} + +DzlJoinedMenu * +dzl_joined_menu_new (void) +{ + return g_object_new (DZL_TYPE_JOINED_MENU, NULL); +} + +static void +dzl_joined_menu_insert (DzlJoinedMenu *self, + GMenuModel *model, + gint index) +{ + Menu menu = { 0 }; + gint offset; + gint n_items; + + g_assert (DZL_IS_JOINED_MENU (self)); + g_assert (G_IS_MENU_MODEL (model)); + g_assert (index >= 0); + g_assert (index <= self->menus->len); + + menu.model = g_object_ref (model); + menu.items_changed_handler = + g_signal_connect_swapped (menu.model, + "items-changed", + G_CALLBACK (dzl_joined_menu_on_items_changed), + self); + g_array_insert_val (self->menus, index, menu); + + n_items = g_menu_model_get_n_items (model); + offset = dzl_joined_menu_get_offset_at_index (self, index); + g_menu_model_items_changed (G_MENU_MODEL (self), offset, 0, n_items); +} + +void +dzl_joined_menu_append_menu (DzlJoinedMenu *self, + GMenuModel *model) +{ + + g_return_if_fail (DZL_IS_JOINED_MENU (self)); + g_return_if_fail (G_MENU_MODEL (model)); + + dzl_joined_menu_insert (self, model, self->menus->len); +} + +void +dzl_joined_menu_prepend_menu (DzlJoinedMenu *self, + GMenuModel *model) +{ + g_return_if_fail (DZL_IS_JOINED_MENU (self)); + g_return_if_fail (G_MENU_MODEL (model)); + + dzl_joined_menu_insert (self, model, 0); +} + +void +dzl_joined_menu_remove_index (DzlJoinedMenu *self, + guint index) +{ + const Menu *menu; + gint n_items; + gint offset; + + g_return_if_fail (DZL_IS_JOINED_MENU (self)); + g_return_if_fail (index < self->menus->len); + + menu = &g_array_index (self->menus, Menu, index); + + offset = dzl_joined_menu_get_offset_at_index (self, index); + n_items = g_menu_model_get_n_items (menu->model); + + g_array_remove_index (self->menus, index); + + g_menu_model_items_changed (G_MENU_MODEL (self), offset, n_items, 0); +} + +void +dzl_joined_menu_remove_menu (DzlJoinedMenu *self, + GMenuModel *model) +{ + g_return_if_fail (DZL_IS_JOINED_MENU (self)); + g_return_if_fail (G_IS_MENU_MODEL (model)); + + for (guint i = 0; i < self->menus->len; i++) + { + if (g_array_index (self->menus, Menu, i).model == model) + { + dzl_joined_menu_remove_index (self, i); + break; + } + } +} + +/** + * dzl_joined_menu_get_n_joined: + * @self: a #DzlJoinedMenu + * + * Gets the number of joined menus. + */ +guint +dzl_joined_menu_get_n_joined (DzlJoinedMenu *self) +{ + g_return_val_if_fail (DZL_IS_JOINED_MENU (self), 0); + + return self->menus->len; +} diff --git a/src/menus/dzl-joined-menu.h b/src/menus/dzl-joined-menu.h new file mode 100644 index 0000000..008d00f --- /dev/null +++ b/src/menus/dzl-joined-menu.h @@ -0,0 +1,52 @@ +/* dzl-joined-menu.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_JOINED_MENU_H +#define DZL_JOINED_MENU_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_JOINED_MENU (dzl_joined_menu_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlJoinedMenu, dzl_joined_menu, DZL, JOINED_MENU, GMenuModel) + +DZL_AVAILABLE_IN_ALL +DzlJoinedMenu *dzl_joined_menu_new (void); +DZL_AVAILABLE_IN_ALL +guint dzl_joined_menu_get_n_joined (DzlJoinedMenu *self); +DZL_AVAILABLE_IN_ALL +void dzl_joined_menu_append_menu (DzlJoinedMenu *self, + GMenuModel *model); +DZL_AVAILABLE_IN_ALL +void dzl_joined_menu_prepend_menu (DzlJoinedMenu *self, + GMenuModel *model); +DZL_AVAILABLE_IN_ALL +void dzl_joined_menu_remove_menu (DzlJoinedMenu *self, + GMenuModel *model); +DZL_AVAILABLE_IN_ALL +void dzl_joined_menu_remove_index (DzlJoinedMenu *self, + guint index); + +G_END_DECLS + +#endif /* DZL_JOINED_MENU_H */ diff --git a/src/menus/dzl-menu-button-item.c b/src/menus/dzl-menu-button-item.c new file mode 100644 index 0000000..c3099d9 --- /dev/null +++ b/src/menus/dzl-menu-button-item.c @@ -0,0 +1,303 @@ +/* dzl-menu-button-item.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-menu-button-item" + +#include "config.h" + +#include "menus/dzl-menu-button.h" +#include "menus/dzl-menu-button-item.h" +#include "shortcuts/dzl-shortcut-label.h" +#include "shortcuts/dzl-shortcut-simple-label.h" +#include "shortcuts/dzl-shortcut-private.h" +#include "util/dzl-gtk.h" + +struct _DzlMenuButtonItem +{ + GtkCheckButton parent_instance; + + const gchar *action_name; + + /* Template references */ + DzlShortcutSimpleLabel *accel; + GtkImage *image; + + /* -1 is for unset, otherwise GtkButtonRole */ + gint role; + + guint has_icon : 1; + guint show_image : 1; +}; + +enum { + PROP_0, + PROP_ACCEL, + PROP_ICON_NAME, + PROP_ROLE, + PROP_SHOW_ACCEL, + PROP_SHOW_IMAGE, + PROP_TEXT_SIZE_GROUP, + PROP_TEXT, + N_PROPS, +}; + +G_DEFINE_TYPE (DzlMenuButtonItem, dzl_menu_button_item, GTK_TYPE_CHECK_BUTTON) + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_menu_button_item_clicked (DzlMenuButtonItem *self) +{ + gboolean transitions_enabled = FALSE; + GtkWidget *button; + GtkWidget *popover; + + g_assert (DZL_IS_MENU_BUTTON_ITEM (self)); + + button = dzl_gtk_widget_get_relative (GTK_WIDGET (self), DZL_TYPE_MENU_BUTTON); + if (button != NULL) + g_object_get (button, "transitions-enabled", &transitions_enabled, NULL); + + popover = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_POPOVER); + + if (transitions_enabled) + gtk_popover_popdown (GTK_POPOVER (popover)); + else + gtk_widget_hide (popover); +} + +static gboolean +action_is_stateful (GtkWidget *widget, + const gchar *group, + const gchar *name) +{ + GActionGroup *actions = gtk_widget_get_action_group (widget, group); + GtkWidget *parent; + + if (actions != NULL) + { + if (g_action_group_has_action (actions, name) && + g_action_group_get_action_state_type (actions, name) != NULL) + return TRUE; + } + + if (GTK_IS_POPOVER (widget)) + parent = gtk_popover_get_relative_to (GTK_POPOVER (widget)); + else + parent = gtk_widget_get_parent (widget); + + if (parent) + return action_is_stateful (parent, group, name); + + return FALSE; +} + +static void +dzl_menu_button_item_notify_action_name (DzlMenuButtonItem *self, + GParamSpec *pspec) +{ + const gchar *action_name; + g_auto(GStrv) parts = NULL; + gboolean draw = FALSE; + + g_assert (DZL_IS_MENU_BUTTON_ITEM (self)); + + action_name = gtk_actionable_get_action_name (GTK_ACTIONABLE (self)); + + if (action_name) + parts = g_strsplit (action_name, ".", 2); + + if (parts && parts[0] && parts[1]) + draw = action_is_stateful (GTK_WIDGET (self), parts[0], parts[1]); + + g_object_set (self, "draw-indicator", draw, NULL); +} + +static void +dzl_menu_button_item_hierarchy_changed (GtkWidget *widget, + GtkWidget *old_toplevel) +{ + DzlMenuButtonItem *self = (DzlMenuButtonItem *)widget; + + g_assert (DZL_IS_MENU_BUTTON_ITEM (self)); + + if (self->role == -1) + dzl_menu_button_item_notify_action_name (self, NULL); +} + +static void +dzl_menu_button_item_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlMenuButtonItem *self = DZL_MENU_BUTTON_ITEM (object); + + switch (prop_id) + { + case PROP_ACCEL: + dzl_shortcut_simple_label_set_accel (self->accel, g_value_get_string (value)); + break; + + case PROP_ICON_NAME: + self->has_icon = !!g_value_get_string (value); + g_object_set_property (G_OBJECT (self->image), "icon-name", value); + gtk_widget_set_visible (GTK_WIDGET (self->image), self->has_icon && self->show_image); + break; + + case PROP_ROLE: + self->role = g_value_get_int (value); + if (self->role == GTK_BUTTON_ROLE_CHECK) + g_object_set (self, "draw-indicator", TRUE, NULL); + else + { + g_object_set (self, "draw-indicator", FALSE, NULL); + if (self->role == -1) + dzl_menu_button_item_hierarchy_changed (GTK_WIDGET (self), NULL); + } + break; + + case PROP_SHOW_ACCEL: + g_object_set_property (G_OBJECT (self->accel), "show-accel", value); + break; + + case PROP_SHOW_IMAGE: + self->show_image = g_value_get_boolean (value); + gtk_widget_set_visible (GTK_WIDGET (self->image), self->has_icon && self->show_image); + break; + + case PROP_TEXT: + dzl_shortcut_simple_label_set_title (self->accel, g_value_get_string (value)); + break; + + case PROP_TEXT_SIZE_GROUP: + _dzl_shortcut_simple_label_set_size_group (self->accel, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_menu_button_item_class_init (DzlMenuButtonItemClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = dzl_menu_button_item_set_property; + + widget_class->hierarchy_changed = dzl_menu_button_item_hierarchy_changed; + + properties [PROP_ACCEL] = + g_param_spec_string ("accel", + "Accel", + "The accelerator for the item", + NULL, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "Icon Name", + "The icon to display with the item", + NULL, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_ROLE] = + g_param_spec_int ("role", NULL, NULL, + -1, GTK_BUTTON_ROLE_RADIO, -1, + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_ACCEL] = + g_param_spec_boolean ("show-accel", + "Show Accel", + "If the accel label should be shown", + FALSE, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_IMAGE] = + g_param_spec_boolean ("show-image", + "Show Image", + "If the image should be shown", + FALSE, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TEXT] = + g_param_spec_string ("text", + "Text", + "The text for the menu item", + NULL, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TEXT_SIZE_GROUP] = + g_param_spec_object ("text-size-group", NULL, NULL, + GTK_TYPE_SIZE_GROUP, + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_menu_button_item_init (DzlMenuButtonItem *self) +{ + GtkTextDirection dir; + GtkBox *box; + + self->role = -1; + + dzl_gtk_widget_add_style_class (GTK_WIDGET (self), "dzlmenubuttonitem"); + + g_signal_connect (self, + "clicked", + G_CALLBACK (dzl_menu_button_item_clicked), + NULL); + + g_signal_connect (self, + "notify::action-name", + G_CALLBACK (dzl_menu_button_item_notify_action_name), + NULL); + + /* flip the location of the checkbutton */ + dir = gtk_widget_get_direction (GTK_WIDGET (self)); + if (dir != GTK_TEXT_DIR_LTR) + dir = GTK_TEXT_DIR_LTR; + else + dir = GTK_TEXT_DIR_RTL; + gtk_widget_set_direction (GTK_WIDGET (self), dir); + + g_object_set (self, "draw-indicator", FALSE, NULL); + + box = g_object_new (GTK_TYPE_BOX, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (box)); + + self->image = g_object_new (GTK_TYPE_IMAGE, + "hexpand", FALSE, + NULL); + gtk_container_add_with_properties (GTK_CONTAINER (box), GTK_WIDGET (self->image), + "pack-type", GTK_PACK_START, + "position", 0, + NULL); + + self->accel = g_object_new (DZL_TYPE_SHORTCUT_SIMPLE_LABEL, + "hexpand", TRUE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (self->accel)); +} diff --git a/src/menus/dzl-menu-button-item.h b/src/menus/dzl-menu-button-item.h new file mode 100644 index 0000000..a7c2930 --- /dev/null +++ b/src/menus/dzl-menu-button-item.h @@ -0,0 +1,33 @@ +/* dzl-menu-button-item.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_MENU_BUTTON_ITEM_H +#define DZL_MENU_BUTTON_ITEM_H + +#include + +G_BEGIN_DECLS + +#define DZL_TYPE_MENU_BUTTON_ITEM (dzl_menu_button_item_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlMenuButtonItem, dzl_menu_button_item, DZL, MENU_BUTTON_ITEM, GtkCheckButton) + +G_END_DECLS + +#endif /* DZL_MENU_BUTTON_ITEM_H */ diff --git a/src/menus/dzl-menu-button-section.c b/src/menus/dzl-menu-button-section.c new file mode 100644 index 0000000..d1e74ba --- /dev/null +++ b/src/menus/dzl-menu-button-section.c @@ -0,0 +1,327 @@ +/* dzl-menu-button-section.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-menu-button-section" + +#include "config.h" + +#include "bindings/dzl-signal-group.h" +#include "menus/dzl-menu-button-section.h" +#include "menus/dzl-menu-button-item.h" +#include "widgets/dzl-box.h" +#include "util/dzl-util-private.h" + +struct _DzlMenuButtonSection +{ + GtkBox parent_instance; + + /* Owned references */ + DzlSignalGroup *menu_signals; + GtkSizeGroup *text_size_group; + + /* Template references */ + GtkLabel *label; + DzlBox *items_box; + + guint show_accels : 1; + guint show_icons : 1; +}; + +enum { + PROP_0, + PROP_LABEL, + PROP_MODEL, + PROP_SHOW_ACCELS, + PROP_SHOW_ICONS, + PROP_TEXT_SIZE_GROUP, + N_PROPS +}; + +G_DEFINE_TYPE (DzlMenuButtonSection, dzl_menu_button_section, GTK_TYPE_BOX) + +static GParamSpec *properties [N_PROPS]; + +static void +update_show_accel (GtkWidget *widget, + DzlMenuButtonSection *self) +{ + if (DZL_IS_MENU_BUTTON_ITEM (widget)) + g_object_set (widget, "show-accel", self->show_accels, NULL); +} + +static void +dzl_menu_button_section_set_show_accels (DzlMenuButtonSection *self, + gboolean show_accels) +{ + g_assert (DZL_IS_MENU_BUTTON_SECTION (self)); + + self->show_accels = !!show_accels; + gtk_container_foreach (GTK_CONTAINER (self->items_box), + (GtkCallback) update_show_accel, + self); +} + +static void +update_show_icon (GtkWidget *widget, + DzlMenuButtonSection *self) +{ + if (DZL_IS_MENU_BUTTON_ITEM (widget)) + g_object_set (widget, "show-image", self->show_icons, NULL); +} + +static void +dzl_menu_button_section_set_show_icons (DzlMenuButtonSection *self, + gboolean show_icons) +{ + g_assert (DZL_IS_MENU_BUTTON_SECTION (self)); + + self->show_icons = !!show_icons; + gtk_container_foreach (GTK_CONTAINER (self->items_box), + (GtkCallback) update_show_icon, + self); +} + +static void +dzl_menu_button_section_items_changed (DzlMenuButtonSection *self, + guint position, + guint removed, + guint added, + GMenuModel *menu) +{ + g_assert (DZL_IS_MENU_BUTTON_SECTION (self)); + g_assert (G_IS_MENU_MODEL (menu)); + + for (guint i = 0; i < removed; i++) + { + GtkWidget *child = dzl_box_get_nth_child (self->items_box, position); + + gtk_widget_destroy (child); + } + + for (guint i = position; i < (position + added); i++) + { + DzlMenuButtonItem *item; + g_autoptr(GVariant) target = NULL; + g_autofree gchar *accel = NULL; + g_autofree gchar *action = NULL; + g_autofree gchar *label = NULL; + g_autofree gchar *verb_icon_name = NULL; + g_autofree gchar *rolestr = NULL; + gint role = -1; + + g_menu_model_get_item_attribute (menu, i, G_MENU_ATTRIBUTE_LABEL, "s", &label); + g_menu_model_get_item_attribute (menu, i, "verb-icon-name", "s", &verb_icon_name); + g_menu_model_get_item_attribute (menu, i, "accel", "s", &accel); + g_menu_model_get_item_attribute (menu, i, "action", "s", &action); + g_menu_model_get_item_attribute (menu, i, "role", "s", &rolestr); + target = g_menu_model_get_item_attribute_value (menu, i, "target", NULL); + + if (g_strcmp0 (rolestr, "check") == 0) + role = GTK_BUTTON_ROLE_CHECK; + else if (g_strcmp0 (rolestr, "normal") == 0) + role = GTK_BUTTON_ROLE_NORMAL; + + item = g_object_new (DZL_TYPE_MENU_BUTTON_ITEM, + "action-name", action, + "action-target", target, + "show-image", self->show_icons, + "show-accel", self->show_accels, + "icon-name", verb_icon_name, + "role", role, + "text", label, + "text-size-group", self->text_size_group, + "accel", accel, + "visible", TRUE, + NULL); + gtk_container_add_with_properties (GTK_CONTAINER (self->items_box), GTK_WIDGET (item), + "position", i, + NULL); + } +} + +static void +dzl_menu_button_section_bind (DzlMenuButtonSection *self, + GMenuModel *menu, + DzlSignalGroup *menu_signals) +{ + guint n_items; + + g_assert (DZL_IS_MENU_BUTTON_SECTION (self)); + g_assert (G_IS_MENU_MODEL (menu)); + g_assert (DZL_IS_SIGNAL_GROUP (menu_signals)); + + /* Remove on bind instead of unbind to avoid data races + * when destroying the widget. + */ + gtk_container_foreach (GTK_CONTAINER (self->items_box), + (GtkCallback) gtk_widget_destroy, + NULL); + + n_items = g_menu_model_get_n_items (menu); + dzl_menu_button_section_items_changed (self, 0, 0, n_items, menu); +} + +static void +dzl_menu_button_section_destroy (GtkWidget *widget) +{ + DzlMenuButtonSection *self = (DzlMenuButtonSection *)widget; + + g_clear_object (&self->menu_signals); + g_clear_object (&self->text_size_group); + + GTK_WIDGET_CLASS (dzl_menu_button_section_parent_class)->destroy (widget); +} + +static void +dzl_menu_button_section_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlMenuButtonSection *self = DZL_MENU_BUTTON_SECTION (object); + + switch (prop_id) + { + case PROP_MODEL: + g_value_set_object (value, dzl_signal_group_get_target (self->menu_signals)); + break; + + case PROP_LABEL: + g_value_set_string (value, gtk_label_get_label (self->label)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_menu_button_section_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlMenuButtonSection *self = DZL_MENU_BUTTON_SECTION (object); + + switch (prop_id) + { + case PROP_MODEL: + dzl_signal_group_set_target (self->menu_signals, g_value_get_object (value)); + break; + + case PROP_LABEL: + gtk_label_set_label (self->label, g_value_get_string (value)); + gtk_widget_set_visible (GTK_WIDGET (self->label), + !dzl_str_empty0 (g_value_get_string (value))); + break; + + case PROP_SHOW_ICONS: + dzl_menu_button_section_set_show_icons (self, g_value_get_boolean (value)); + break; + + case PROP_SHOW_ACCELS: + dzl_menu_button_section_set_show_accels (self, g_value_get_boolean (value)); + break; + + case PROP_TEXT_SIZE_GROUP: + self->text_size_group = g_value_dup_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_menu_button_section_class_init (DzlMenuButtonSectionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = dzl_menu_button_section_get_property; + object_class->set_property = dzl_menu_button_section_set_property; + + widget_class->destroy = dzl_menu_button_section_destroy; + + properties [PROP_SHOW_ACCELS] = + g_param_spec_boolean ("show-accels", NULL, NULL, FALSE, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_ICONS] = + g_param_spec_boolean ("show-icons", NULL, NULL, FALSE, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_MODEL] = + g_param_spec_object ("model", NULL, NULL, + G_TYPE_MENU_MODEL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_LABEL] = + g_param_spec_string ("label", NULL, NULL, NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TEXT_SIZE_GROUP] = + g_param_spec_object ("text-size-group", NULL, NULL, + GTK_TYPE_SIZE_GROUP, + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_css_name (widget_class, "dzlmenubuttonsection"); + gtk_widget_class_set_template_from_resource (widget_class, + "/org/gnome/dazzle/ui/dzl-menu-button-section.ui"); + gtk_widget_class_bind_template_child (widget_class, DzlMenuButtonSection, label); + gtk_widget_class_bind_template_child (widget_class, DzlMenuButtonSection, items_box); +} + +static void +dzl_menu_button_section_init (DzlMenuButtonSection *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->menu_signals = dzl_signal_group_new (G_TYPE_MENU_MODEL); + + g_signal_connect_swapped (self->menu_signals, + "bind", + G_CALLBACK (dzl_menu_button_section_bind), + self); + + dzl_signal_group_connect_swapped (self->menu_signals, + "items-changed", + G_CALLBACK (dzl_menu_button_section_items_changed), + self); +} + +/** + * dzl_menu_button_section_new: + * + * Creates a new #DzlMenuButtonSection. + * + * Returns: (transfer full): A #DzlMenuButtonSection + * + * Since: 3.26 + */ +GtkWidget * +dzl_menu_button_section_new (GMenuModel *model, + const gchar *label) +{ + return g_object_new (DZL_TYPE_MENU_BUTTON_SECTION, + "model", model, + "label", label, + NULL); +} diff --git a/src/menus/dzl-menu-button-section.h b/src/menus/dzl-menu-button-section.h new file mode 100644 index 0000000..0482ad8 --- /dev/null +++ b/src/menus/dzl-menu-button-section.h @@ -0,0 +1,35 @@ +/* dzl-menu-button-section.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_MENU_BUTTON_SECTION_H +#define DZL_MENU_BUTTON_SECTION_H + +#include + +G_BEGIN_DECLS + +#define DZL_TYPE_MENU_BUTTON_SECTION (dzl_menu_button_section_get_type()) + +G_DECLARE_FINAL_TYPE (DzlMenuButtonSection, dzl_menu_button_section, DZL, MENU_BUTTON_SECTION, GtkBox) + +GtkWidget *dzl_menu_button_section_new (GMenuModel *model, + const gchar *label); + +G_END_DECLS + +#endif /* DZL_MENU_BUTTON_SECTION_H */ diff --git a/src/menus/dzl-menu-button-section.ui b/src/menus/dzl-menu-button-section.ui new file mode 100644 index 0000000..d9089df --- /dev/null +++ b/src/menus/dzl-menu-button-section.ui @@ -0,0 +1,26 @@ + + + + diff --git a/src/menus/dzl-menu-button.c b/src/menus/dzl-menu-button.c new file mode 100644 index 0000000..9a8018d --- /dev/null +++ b/src/menus/dzl-menu-button.c @@ -0,0 +1,579 @@ +/* dzl-menu-button.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-menu-button" + +#include "config.h" + +#include "app/dzl-application.h" +#include "bindings/dzl-signal-group.h" +#include "menus/dzl-menu-button.h" +#include "menus/dzl-menu-button-section.h" +#include "menus/dzl-menu-button-item.h" +#include "util/dzl-gtk.h" +#include "widgets/dzl-box.h" + +typedef struct +{ + /* Owned references */ + DzlSignalGroup *menu_signals; + + /* Template references */ + GtkPopover *popover; + GtkImage *image; + GtkImage *pan_down_image; + DzlBox *popover_box; + GtkSizeGroup *text_size_group; + + guint show_accels : 1; + guint show_icons : 1; + guint transitions_enabled : 1; +} DzlMenuButtonPrivate; + +enum { + PROP_0, + PROP_MODEL, + PROP_MENU_ID, + PROP_ICON_NAME, + PROP_SHOW_ACCELS, + PROP_SHOW_ARROW, + PROP_SHOW_ICONS, + PROP_TRANSITIONS_ENABLED, + N_PROPS +}; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlMenuButton, dzl_menu_button, GTK_TYPE_MENU_BUTTON) + +static GParamSpec *properties [N_PROPS]; + +static void +collect_items_sections (GtkWidget *widget, + DzlMenuButton *self) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + if (DZL_IS_MENU_BUTTON_SECTION (widget)) + g_object_set (widget, + "show-accels", priv->show_accels, + "show-icons", priv->show_icons, + NULL); +} + +static void +update_image_and_accels (DzlMenuButton *self) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + g_assert (DZL_IS_MENU_BUTTON (self)); + + gtk_container_foreach (GTK_CONTAINER (priv->popover_box), + (GtkCallback) collect_items_sections, + self); +} + +static void +dzl_menu_button_add_linked_model (DzlMenuButton *self, + guint position, + GMenuModel *model, + const gchar *label) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + DzlMenuButtonSection *section; + + g_assert (DZL_IS_MENU_BUTTON (self)); + g_assert (G_IS_MENU_MODEL (model)); + + section = g_object_new (DZL_TYPE_MENU_BUTTON_SECTION, + "label", label, + "model", model, + "show-accels", priv->show_accels, + "show-icons", priv->show_icons, + "text-size-group", priv->text_size_group, + "visible", TRUE, + NULL); + gtk_container_add_with_properties (GTK_CONTAINER (priv->popover_box), GTK_WIDGET (section), + "position", position, + NULL); +} + +static void +dzl_menu_button_items_changed (DzlMenuButton *self, + guint position, + guint removed, + guint added, + GMenuModel *menu) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + GList *children = NULL; + + g_assert (DZL_IS_MENU_BUTTON (self)); + g_assert (G_IS_MENU_MODEL (menu)); + + for (guint i = 0; i < removed; i++) + { + GtkWidget *child = dzl_box_get_nth_child (priv->popover_box, position + i); + children = g_list_prepend (children, g_object_ref (child)); + } + + g_list_foreach (children, (GFunc)gtk_widget_destroy, NULL); + g_list_free_full (children, g_object_unref); + + for (guint i = position; i < (position + added); i++) + { + g_autofree gchar *label = NULL; + g_autoptr(GMenuModel) linked_model = NULL; + + /* We only support sections at the top-level */ + g_menu_model_get_item_attribute (menu, i, G_MENU_ATTRIBUTE_LABEL, "s", &label); + linked_model = g_menu_model_get_item_link (menu, i, G_MENU_LINK_SECTION); + + if (linked_model != NULL) + dzl_menu_button_add_linked_model (self, i, linked_model, label); + } + + update_image_and_accels (self); +} + +static void +dzl_menu_button_menu_signals_bind (DzlMenuButton *self, + GMenuModel *menu, + DzlSignalGroup *menu_signals) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + guint n_items; + + g_assert (DZL_IS_MENU_BUTTON (self)); + g_assert (G_IS_MENU_MODEL (menu)); + g_assert (DZL_IS_SIGNAL_GROUP (menu_signals)); + + /* Clear on bind instead of unbind to avoid data races. + * We already are insensitive when unbound, so this should + * be a fine solution. + */ + gtk_container_foreach (GTK_CONTAINER (priv->popover_box), + (GtkCallback) gtk_widget_destroy, + NULL); + + n_items = g_menu_model_get_n_items (menu); + dzl_menu_button_items_changed (self, 0, 0, n_items, menu); + + gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE); +} + +static void +dzl_menu_button_menu_signals_unbind (DzlMenuButton *self, + DzlSignalGroup *menu_signals) +{ + g_assert (DZL_IS_MENU_BUTTON (self)); + g_assert (DZL_IS_SIGNAL_GROUP (menu_signals)); + + gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE); +} + +static void +dzl_menu_button_set_menu_id (DzlMenuButton *self, + const gchar *menu_id) +{ + GApplication *app; + GMenu *model = NULL; + + g_return_if_fail (DZL_IS_MENU_BUTTON (self)); + + if (menu_id == NULL) + { + dzl_menu_button_set_model (self, NULL); + return; + } + + app = g_application_get_default (); + + if (DZL_IS_APPLICATION (app)) + model = dzl_application_get_menu_by_id (DZL_APPLICATION (app), menu_id); + else if (GTK_IS_APPLICATION (app)) + model = gtk_application_get_menu_by_id (GTK_APPLICATION (app), menu_id); + + dzl_menu_button_set_model (self, G_MENU_MODEL (model)); +} + +static void +dzl_menu_button_destroy (GtkWidget *widget) +{ + DzlMenuButton *self = (DzlMenuButton *)widget; + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + g_clear_object (&priv->menu_signals); + + GTK_WIDGET_CLASS (dzl_menu_button_parent_class)->destroy (widget); +} + +static void +dzl_menu_button_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlMenuButton *self = DZL_MENU_BUTTON (object); + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + switch (prop_id) + { + case PROP_MODEL: + g_value_set_object (value, dzl_menu_button_get_model (self)); + break; + + case PROP_SHOW_ARROW: + g_value_set_boolean (value, dzl_menu_button_get_show_arrow (self)); + break; + + case PROP_SHOW_ACCELS: + g_value_set_boolean (value, dzl_menu_button_get_show_accels (self)); + break; + + case PROP_SHOW_ICONS: + g_value_set_boolean (value, dzl_menu_button_get_show_icons (self)); + break; + + case PROP_TRANSITIONS_ENABLED: + g_value_set_boolean (value, priv->transitions_enabled); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_menu_button_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlMenuButton *self = DZL_MENU_BUTTON (object); + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + switch (prop_id) + { + case PROP_MODEL: + dzl_menu_button_set_model (self, g_value_get_object (value)); + break; + + case PROP_MENU_ID: + dzl_menu_button_set_menu_id (self, g_value_get_string (value)); + break; + + case PROP_ICON_NAME: + g_object_set_property (G_OBJECT (priv->image), "icon-name", value); + break; + + case PROP_SHOW_ARROW: + dzl_menu_button_set_show_arrow (self, g_value_get_boolean (value)); + break; + + case PROP_SHOW_ACCELS: + dzl_menu_button_set_show_accels (self, g_value_get_boolean (value)); + break; + + case PROP_SHOW_ICONS: + dzl_menu_button_set_show_icons (self, g_value_get_boolean (value)); + break; + + case PROP_TRANSITIONS_ENABLED: + priv->transitions_enabled = g_value_get_boolean (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_menu_button_class_init (DzlMenuButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = dzl_menu_button_get_property; + object_class->set_property = dzl_menu_button_set_property; + + widget_class->destroy = dzl_menu_button_destroy; + + properties [PROP_TRANSITIONS_ENABLED] = + g_param_spec_boolean ("transitions-enabled", + "Transitions Enabled", + "If transitions should be allowed", + TRUE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlMenuButton:menu-id: + * + * The "menu-id" property can be used to automatically load a + * #GMenuModel from the applications merged menus. This is + * performed via dzl_application_get_menu_by_id(). + * + * Since: 3.26 + */ + properties [PROP_MENU_ID] = + g_param_spec_string ("menu-id", + "Menu Id", + "The identifier for the menu model to use", + NULL, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_MODEL] = + g_param_spec_object ("model", + "Model", + "The GMenuModel to display in the popover", + G_TYPE_MENU_MODEL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "Icon Name", + "The icon-name for the button", + NULL, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_ACCELS] = + g_param_spec_boolean ("show-accels", + "Show Accels", + "If accelerator keys should be shown", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_ARROW] = + g_param_spec_boolean ("show-arrow", + "Show Arrow", + "If the down arrow should be shown", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_ICONS] = + g_param_spec_boolean ("show-icons", + "Show Icons", + "If icons should be shown next to menu items", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-menu-button.ui"); + gtk_widget_class_bind_template_child_private (widget_class, DzlMenuButton, image); + gtk_widget_class_bind_template_child_private (widget_class, DzlMenuButton, pan_down_image); + gtk_widget_class_bind_template_child_private (widget_class, DzlMenuButton, popover); + gtk_widget_class_bind_template_child_private (widget_class, DzlMenuButton, popover_box); + gtk_widget_class_bind_template_child_private (widget_class, DzlMenuButton, text_size_group); +} + +static void +dzl_menu_button_init (DzlMenuButton *self) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + priv->transitions_enabled = TRUE; + + gtk_widget_init_template (GTK_WIDGET (self)); + + priv->menu_signals = dzl_signal_group_new (G_TYPE_MENU_MODEL); + + g_signal_connect_swapped (priv->menu_signals, + "bind", + G_CALLBACK (dzl_menu_button_menu_signals_bind), + self); + + g_signal_connect_swapped (priv->menu_signals, + "unbind", + G_CALLBACK (dzl_menu_button_menu_signals_unbind), + self); + + dzl_signal_group_connect_swapped (priv->menu_signals, + "items-changed", + G_CALLBACK (dzl_menu_button_items_changed), + self); + + gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE); +} + +/** + * dzl_menu_button_new_with_model: + * @icon_name: An icon-name for the button + * @model: (nullable): A #GMenuModel or %NULL + * + * Creates a new #DzlMenuButton with the icon @icon_name and + * the menu contents of @model. + * + * Returns: (transfer full): A #DzlMenuButton + */ +GtkWidget * +dzl_menu_button_new_with_model (const gchar *icon_name, + GMenuModel *model) +{ + g_return_val_if_fail (!model || G_IS_MENU_MODEL (model), NULL); + + return g_object_new (DZL_TYPE_MENU_BUTTON, + "icon-name", icon_name, + "model", model, + NULL); +} + +gboolean +dzl_menu_button_get_show_arrow (DzlMenuButton *self) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_MENU_BUTTON (self), FALSE); + + return gtk_widget_get_visible (GTK_WIDGET (priv->pan_down_image)); +} + +/** + * dzl_menu_button_set_show_arrow: + * @self: a #DzlMenuButton + * + * Sets the #DzlMenuButton:show-arrow property. + * + * If %TRUE, an pan-down-symbolic image will be displayed next to the + * image in the button. + * + * Since: 3.26 + */ +void +dzl_menu_button_set_show_arrow (DzlMenuButton *self, + gboolean show_arrow) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + g_return_if_fail (DZL_IS_MENU_BUTTON (self)); + + gtk_widget_set_visible (GTK_WIDGET (priv->pan_down_image), show_arrow); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_ARROW]); +} + +gboolean +dzl_menu_button_get_show_icons (DzlMenuButton *self) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_MENU_BUTTON (self), FALSE); + + return priv->show_icons; +} + +/** + * dzl_menu_button_set_show_icons: + * @self: a #DzlMenuButton + * @show_icons: if icons should be visible + * + * Sets the #DzlMenuButton:show-icons property. + * + * If %TRUE, icons will be displayed next to menu items that + * contain a shortcut. + * + * Since: 3.26 + */ +void +dzl_menu_button_set_show_icons (DzlMenuButton *self, + gboolean show_icons) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + g_return_if_fail (DZL_IS_MENU_BUTTON (self)); + + show_icons = !!show_icons; + + if (priv->show_icons != show_icons) + { + priv->show_icons = show_icons; + update_image_and_accels (self); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_ICONS]); + } +} + +gboolean +dzl_menu_button_get_show_accels (DzlMenuButton *self) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_MENU_BUTTON (self), FALSE); + + return priv->show_accels; +} + +/** + * dzl_menu_button_set_show_accels: + * @self: a #DzlMenuButton + * @show_accels: if accelerators should be visible + * + * Sets the #DzlMenuButton:show-accels property. + * + * If %TRUE, accelerators will be displayed next to menu items that + * contain a shortcut. + * + * Since: 3.26 + */ +void +dzl_menu_button_set_show_accels (DzlMenuButton *self, + gboolean show_accels) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + g_return_if_fail (DZL_IS_MENU_BUTTON (self)); + + show_accels = !!show_accels; + + if (priv->show_accels != show_accels) + { + priv->show_accels = show_accels; + update_image_and_accels (self); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_ICONS]); + } +} + +void +dzl_menu_button_set_model (DzlMenuButton *self, + GMenuModel *model) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + g_return_if_fail (DZL_IS_MENU_BUTTON (self)); + g_return_if_fail (!model || G_IS_MENU_MODEL (model)); + + if ((gpointer)model != dzl_signal_group_get_target (priv->menu_signals)) + { + dzl_signal_group_set_target (priv->menu_signals, model); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]); + } +} + +/** + * dzl_menu_button_get_model: + * @self: a #DzlMenuButton + * + * Returns: (transfer none) (nullable): A #DzlMenuButton or %NULL. + * + * Since: 3.26 + */ +GMenuModel * +dzl_menu_button_get_model (DzlMenuButton *self) +{ + DzlMenuButtonPrivate *priv = dzl_menu_button_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_MENU_BUTTON (self), NULL); + + return dzl_signal_group_get_target (priv->menu_signals); +} diff --git a/src/menus/dzl-menu-button.h b/src/menus/dzl-menu-button.h new file mode 100644 index 0000000..c0e6a2f --- /dev/null +++ b/src/menus/dzl-menu-button.h @@ -0,0 +1,69 @@ +/* dzl-menu-button.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_MENU_BUTTON_H +#define DZL_MENU_BUTTON_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_MENU_BUTTON (dzl_menu_button_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlMenuButton, dzl_menu_button, DZL, MENU_BUTTON, GtkMenuButton) + +struct _DzlMenuButtonClass +{ + GtkMenuButtonClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_menu_button_new_with_model (const gchar *icon_name, + GMenuModel *model); +DZL_AVAILABLE_IN_ALL +GMenuModel *dzl_menu_button_get_model (DzlMenuButton *self); +DZL_AVAILABLE_IN_ALL +void dzl_menu_button_set_model (DzlMenuButton *self, + GMenuModel *model); +DZL_AVAILABLE_IN_ALL +gboolean dzl_menu_button_get_show_arrow (DzlMenuButton *self); +DZL_AVAILABLE_IN_ALL +void dzl_menu_button_set_show_arrow (DzlMenuButton *self, + gboolean show_arrow); +DZL_AVAILABLE_IN_ALL +gboolean dzl_menu_button_get_show_icons (DzlMenuButton *self); +DZL_AVAILABLE_IN_ALL +void dzl_menu_button_set_show_icons (DzlMenuButton *self, + gboolean show_icons); +DZL_AVAILABLE_IN_ALL +gboolean dzl_menu_button_get_show_accels (DzlMenuButton *self); +DZL_AVAILABLE_IN_ALL +void dzl_menu_button_set_show_accels (DzlMenuButton *self, + gboolean show_accels); + +G_END_DECLS + +#endif /* DZL_MENU_BUTTON_H */ diff --git a/src/menus/dzl-menu-button.ui b/src/menus/dzl-menu-button.ui new file mode 100644 index 0000000..51f3975 --- /dev/null +++ b/src/menus/dzl-menu-button.ui @@ -0,0 +1,47 @@ + + + + + + + vertical + true + + + + + + horizontal + + diff --git a/src/menus/dzl-menu-manager.c b/src/menus/dzl-menu-manager.c new file mode 100644 index 0000000..541bc71 --- /dev/null +++ b/src/menus/dzl-menu-manager.c @@ -0,0 +1,673 @@ +/* dzl-menu-manager.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-menu-manager" + +#include "config.h" + +#include + +#include "menus/dzl-menu-manager.h" +#include "util/dzl-util-private.h" + +struct _DzlMenuManager +{ + GObject parent_instance; + + guint last_merge_id; + GHashTable *models; +}; + +G_DEFINE_TYPE (DzlMenuManager, dzl_menu_manager, G_TYPE_OBJECT) + +#define DZL_MENU_ATTRIBUTE_BEFORE "before" +#define DZL_MENU_ATTRIBUTE_AFTER "after" +#define DZL_MENU_ATTRIBUTE_MERGE_ID "dazzle-merge-id" + +/** + * DzlMenuManager: + * + * The goal of #DzlMenuManager is to simplify the process of merging multiple + * GtkBuilder .ui files containing menus into a single representation of the + * application menus. Additionally, it provides the ability to "unmerge" + * previously merged menus. + * + * This allows for an application to have plugins which seemlessly extends + * the core application menus. + * + * Implementation notes: + * + * To make this work, we don't use the GMenu instances created by a GtkBuilder + * instance. Instead, we create the menus ourself and recreate section and + * submenu links. This allows the #DzlMenuManager to be in full control of + * the generated menus. + * + * dzl_menu_manager_get_menu_by_id() will always return a #GMenu, however + * that menu may contain no children until something has extended it later + * on during the application process. + * + * Since: 3.26 + */ + +static const gchar * +get_object_id (GObject *object) +{ + g_assert (G_IS_OBJECT (object)); + + if (GTK_IS_BUILDABLE (object)) + return gtk_buildable_get_name (GTK_BUILDABLE (object)); + else + return g_object_get_data (object, "gtk-builder-name"); +} + +static void +dzl_menu_manager_dispose (GObject *object) +{ + DzlMenuManager *self = (DzlMenuManager *)object; + + g_clear_pointer (&self->models, g_hash_table_unref); + + G_OBJECT_CLASS (dzl_menu_manager_parent_class)->dispose (object); +} + +static void +dzl_menu_manager_class_init (DzlMenuManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = dzl_menu_manager_dispose; +} + +static void +dzl_menu_manager_init (DzlMenuManager *self) +{ + self->models = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); +} + +static gint +find_with_attribute_string (GMenuModel *model, + const gchar *attribute, + const gchar *value) +{ + guint n_items; + + g_assert (G_IS_MENU_MODEL (model)); + g_assert (attribute != NULL); + g_assert (value != NULL); + + n_items = g_menu_model_get_n_items (model); + + for (guint i = 0; i < n_items; i++) + { + g_autofree gchar *item_value = NULL; + + if (g_menu_model_get_item_attribute (model, i, attribute, "s", &item_value) && + (g_strcmp0 (value, item_value) == 0)) + return i; + } + + return -1; +} + +static gboolean +dzl_menu_manager_menu_contains (DzlMenuManager *self, + GMenu *menu, + GMenuItem *item) +{ + const gchar *link_id; + const gchar *label; + + g_assert (DZL_IS_MENU_MANAGER (self)); + g_assert (G_IS_MENU (menu)); + g_assert (G_IS_MENU_ITEM (item)); + + /* try to find match by item label */ + if (g_menu_item_get_attribute (item, G_MENU_ATTRIBUTE_LABEL, "&s", &label) && + (find_with_attribute_string (G_MENU_MODEL (menu), G_MENU_ATTRIBUTE_LABEL, label) >= 0)) + return TRUE; + + /* try to find match by item link */ + if (g_menu_item_get_attribute (item, "dzl-link-id", "&s", &link_id) && + (find_with_attribute_string (G_MENU_MODEL (menu), "dzl-link-id", link_id) >= 0)) + return TRUE; + + return FALSE; +} + +static void +model_copy_attributes_to_item (GMenuModel *model, + gint item_index, + GMenuItem *item) +{ + g_autoptr(GMenuAttributeIter) iter = NULL; + const gchar *attr_name; + GVariant *attr_value; + + g_assert (G_IS_MENU_MODEL (model)); + g_assert (item_index >= 0); + g_assert (G_IS_MENU_ITEM (item)); + + if (!(iter = g_menu_model_iterate_item_attributes (model, item_index))) + return; + + while (g_menu_attribute_iter_get_next (iter, &attr_name, &attr_value)) + { + g_menu_item_set_attribute_value (item, attr_name, attr_value); + g_variant_unref (attr_value); + } +} + +static void +model_copy_links_to_item (GMenuModel *model, + guint position, + GMenuItem *item) +{ + g_autoptr(GMenuLinkIter) link_iter = NULL; + + g_assert (G_IS_MENU_MODEL (model)); + g_assert (G_IS_MENU_ITEM (item)); + + link_iter = g_menu_model_iterate_item_links (model, position); + + while (g_menu_link_iter_next (link_iter)) + { + g_autoptr(GMenuModel) link_model = NULL; + const gchar *link_name; + + link_name = g_menu_link_iter_get_name (link_iter); + link_model = g_menu_link_iter_get_value (link_iter); + + g_menu_item_set_link (item, link_name, link_model); + } +} + +static void +menu_move_item_to (GMenu *menu, + guint position, + guint new_position) +{ + g_autoptr(GMenuItem) item = NULL; + + g_assert (G_IS_MENU (menu)); + + item = g_menu_item_new (NULL, NULL); + model_copy_attributes_to_item (G_MENU_MODEL (menu), position, item); + model_copy_links_to_item (G_MENU_MODEL (menu), position, item); + + g_menu_remove (menu, position); + g_menu_insert_item (menu, new_position, item); +} + +static void +dzl_menu_manager_resolve_constraints (GMenu *menu) +{ + GMenuModel *model = (GMenuModel *)menu; + gint n_items; + + g_assert (G_IS_MENU (menu)); + + n_items = (gint)g_menu_model_get_n_items (G_MENU_MODEL (menu)); + + /* + * We start iterating forwards. As we look at each row, we start + * again from the end working backwards to see if we need to be + * moved after that row. + * + * This way we know we see the furthest we might need to jump first. + */ + + for (gint i = 0; i < n_items; i++) + { + g_autofree gchar *i_after = NULL; + + g_menu_model_get_item_attribute (model, i, "after", "s", &i_after); + if (i_after == NULL) + continue; + + /* Work our way backwards from the end back to + * our current position (but not overlapping). + */ + for (gint j = n_items - 1; j > i; j--) + { + g_autofree gchar *j_id = NULL; + g_autofree gchar *j_label = NULL; + + g_menu_model_get_item_attribute (model, j, "id", "s", &j_id); + g_menu_model_get_item_attribute (model, j, "label", "s", &j_label); + + if (dzl_str_equal0 (i_after, j_id) || dzl_str_equal0 (i_after, j_label)) + { + /* You might think we need to place the item *AFTER* + * our position "j". But since we remove the row where + * "i" currently is, we get the proper location. + */ + menu_move_item_to (menu, i, j); + i--; + break; + } + } + } + + /* + * Now we need to apply the same thing but for the "before" links + * in our model. To do this, we also want to ensure we find the + * furthest jump first. So we start from the end and work our way + * towards the front and for each of those nodes, start from the + * front and work our way back. + */ + + for (gint i = n_items - 1; i >= 0; i--) + { + g_autofree gchar *i_before = NULL; + + g_menu_model_get_item_attribute (model, i, "before", "s", &i_before); + if (i_before == NULL) + continue; + + /* Work our way from the front back towards our current position + * that would cause our position to jump. + */ + for (gint j = 0; j < i; j++) + { + g_autofree gchar *j_id = NULL; + g_autofree gchar *j_label = NULL; + + g_menu_model_get_item_attribute (model, j, "id", "s", &j_id); + g_menu_model_get_item_attribute (model, j, "label", "s", &j_label); + + if (dzl_str_equal0 (i_before, j_id) || dzl_str_equal0 (i_before, j_label)) + { + /* + * This item needs to be placed before this item we just found. + * Since that is the furthest we could jump, just stop + * afterwards. + */ + menu_move_item_to (menu, i, j); + i++; + break; + } + } + } +} + +static void +dzl_menu_manager_add_to_menu (DzlMenuManager *self, + GMenu *menu, + GMenuItem *item) +{ + g_assert (DZL_IS_MENU_MANAGER (self)); + g_assert (G_IS_MENU (menu)); + g_assert (G_IS_MENU_ITEM (item)); + + /* + * The proplem here is one that could end up being an infinite + * loop if we tried to resolve all the position requirements + * until no more position changes were required. So instead we + * simplify the problem into an append, and two-passes as trying + * to fix up the positions. + */ + g_menu_append_item (menu, item); + dzl_menu_manager_resolve_constraints (menu); + dzl_menu_manager_resolve_constraints (menu); +} + +static void +dzl_menu_manager_merge_model (DzlMenuManager *self, + GMenu *menu, + GMenuModel *model, + guint merge_id) +{ + guint n_items; + + g_assert (DZL_IS_MENU_MANAGER (self)); + g_assert (G_IS_MENU (menu)); + g_assert (G_IS_MENU_MODEL (model)); + g_assert (merge_id > 0); + + /* + * NOTES: + * + * Instead of using g_menu_item_new_from_model(), we create our own item + * and resolve section/submenu links. This allows us to be in full control + * of all of the menu items created. + * + * We move through each item in @model. If that item does not exist within + * @menu, we add it taking into account %DZL_MENU_ATTRIBUTE_BEFORE and + * %DZL_MENU_ATTRIBUTE_AFTER. + */ + + n_items = g_menu_model_get_n_items (model); + + for (guint i = 0; i < n_items; i++) + { + g_autoptr(GMenuItem) item = NULL; + g_autoptr(GMenuLinkIter) link_iter = NULL; + + item = g_menu_item_new (NULL, NULL); + + /* + * Copy attributes from the model. This includes, label, action, + * target, before, after, etc. Also set our merge-id so that we + * can remove the item when we are unmerged. + */ + model_copy_attributes_to_item (model, i, item); + g_menu_item_set_attribute (item, DZL_MENU_ATTRIBUTE_MERGE_ID, "u", merge_id); + + /* + * If this is a link, resolve it from our already created GMenu. + * The menu might be empty now, but it will get filled in on a + * followup pass for that model. + */ + link_iter = g_menu_model_iterate_item_links (model, i); + while (g_menu_link_iter_next (link_iter)) + { + g_autoptr(GMenuModel) link_model = NULL; + const gchar *link_name; + const gchar *link_id; + GMenuModel *internal_menu; + + link_name = g_menu_link_iter_get_name (link_iter); + link_model = g_menu_link_iter_get_value (link_iter); + + g_assert (link_name != NULL); + g_assert (G_IS_MENU_MODEL (link_model)); + + link_id = get_object_id (G_OBJECT (link_model)); + + if (link_id == NULL) + { + g_warning ("Link of type \"%s\" missing \"id=\". " + "Merging will not be possible.", + link_name); + continue; + } + + internal_menu = g_hash_table_lookup (self->models, link_id); + + if (internal_menu == NULL) + { + g_warning ("linked menu %s has not been created", link_id); + continue; + } + + /* + * Save the internal link reference-id to do merging of items + * later on. We need to know if an item matches when we might + * not have a "label" to work from. + */ + g_menu_item_set_attribute (item, "dzl-link-id", "s", link_id); + + g_menu_item_set_link (item, link_name, internal_menu); + } + + /* + * If the menu already has this item, that's fine. We will populate + * the submenu/section links in followup merges of their GMenuModel. + */ + if (dzl_menu_manager_menu_contains (self, menu, item)) + continue; + + dzl_menu_manager_add_to_menu (self, menu, item); + } +} + +static void +dzl_menu_manager_merge_builder (DzlMenuManager *self, + GtkBuilder *builder, + guint merge_id) +{ + const GSList *iter; + GSList *list; + + g_assert (DZL_IS_MENU_MANAGER (self)); + g_assert (GTK_IS_BUILDER (builder)); + g_assert (merge_id > 0); + + /* + * NOTES: + * + * We cannot re-use any of the created GMenu from the builder as we need + * control over all the created GMenu. Primarily because manipulating + * existing GMenu is such a PITA. So instead, we create our own GMenu and + * resolve links manually. + * + * Since GtkBuilder requires that all menus have an "id" element, we can + * resolve the menu->id fairly easily. First we create our own GMenu + * instances so that we can always resolve them during the creation process. + * Then we can go through and manually resolve links as we create items. + * + * We don't need to recursively create the menus since we will come across + * additional GMenu instances while iterating the available objects from the + * GtkBuilder. This does require 2 iterations of the objects, but that is + * not an issue. + */ + + list = gtk_builder_get_objects (builder); + + /* + * For every menu with an id, check to see if we already created our + * instance of that menu. If not, create it now so we can resolve them + * while building the menu links. + */ + for (iter = list; iter != NULL; iter = iter->next) + { + GObject *object = iter->data; + const gchar *id; + GMenu *menu; + + if (!G_IS_MENU (object)) + continue; + + if (!(id = get_object_id (object))) + { + g_warning ("menu without identifier, implausible"); + continue; + } + + if (!(menu = g_hash_table_lookup (self->models, id))) + g_hash_table_insert (self->models, g_strdup (id), g_menu_new ()); + } + + /* + * Now build each menu we discovered in the GtkBuilder. We do not need to + * build them recursively since we will pass the linked menus as we make + * forward progress on the GtkBuilder created objects. + */ + + for (iter = list; iter != NULL; iter = iter->next) + { + GObject *object = iter->data; + const gchar *id; + GMenu *menu; + + if (!G_IS_MENU_MODEL (object)) + continue; + + if (!(id = get_object_id (object))) + continue; + + menu = g_hash_table_lookup (self->models, id); + + g_assert (G_IS_MENU (menu)); + + dzl_menu_manager_merge_model (self, menu, G_MENU_MODEL (object), merge_id); + } + + g_slist_free (list); +} + +DzlMenuManager * +dzl_menu_manager_new (void) +{ + return g_object_new (DZL_TYPE_MENU_MANAGER, NULL); +} + +guint +dzl_menu_manager_add_filename (DzlMenuManager *self, + const gchar *filename, + GError **error) +{ + GtkBuilder *builder; + guint merge_id; + + g_return_val_if_fail (DZL_IS_MENU_MANAGER (self), 0); + g_return_val_if_fail (filename != NULL, 0); + + builder = gtk_builder_new (); + + if (!gtk_builder_add_from_file (builder, filename, error)) + { + g_object_unref (builder); + return 0; + } + + merge_id = ++self->last_merge_id; + dzl_menu_manager_merge_builder (self, builder, merge_id); + g_object_unref (builder); + + return merge_id; +} + +guint +dzl_menu_manager_merge (DzlMenuManager *self, + const gchar *menu_id, + GMenuModel *menu_model) +{ + GMenu *menu; + guint merge_id; + + g_return_val_if_fail (DZL_IS_MENU_MANAGER (self), 0); + g_return_val_if_fail (menu_id != NULL, 0); + g_return_val_if_fail (G_IS_MENU_MODEL (menu_model), 0); + + merge_id = ++self->last_merge_id; + + if (!(menu = g_hash_table_lookup (self->models, menu_id))) + { + GMenu *new_model = g_menu_new (); + g_hash_table_insert (self->models, g_strdup (menu_id), new_model); + menu = new_model; + } + + dzl_menu_manager_merge_model (self, menu, menu_model, merge_id); + + return merge_id; +} + +/** + * dzl_menu_manager_remove: + * @self: a #DzlMenuManager + * @merge_id: A previously registered merge id + * + * This removes items from menus that were added as part of a previous + * menu merge. Use the value returned from dzl_menu_manager_merge() as + * the @merge_id. + * + * Since: 3.26 + */ +void +dzl_menu_manager_remove (DzlMenuManager *self, + guint merge_id) +{ + GHashTableIter iter; + GMenu *menu; + + g_return_if_fail (DZL_IS_MENU_MANAGER (self)); + g_return_if_fail (merge_id != 0); + + g_hash_table_iter_init (&iter, self->models); + + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&menu)) + { + gint n_items; + gint i; + + g_assert (G_IS_MENU (menu)); + + n_items = g_menu_model_get_n_items (G_MENU_MODEL (menu)); + + /* Iterate backward so we have a stable loop variable. */ + for (i = n_items - 1; i >= 0; i--) + { + guint item_merge_id = 0; + + if (g_menu_model_get_item_attribute (G_MENU_MODEL (menu), + i, + DZL_MENU_ATTRIBUTE_MERGE_ID, + "u", &item_merge_id)) + { + if (item_merge_id == merge_id) + g_menu_remove (menu, i); + } + } + } +} + +/** + * dzl_menu_manager_get_menu_by_id: + * + * Returns: (transfer none): A #GMenu. + */ +GMenu * +dzl_menu_manager_get_menu_by_id (DzlMenuManager *self, + const gchar *menu_id) +{ + GMenu *menu; + + g_return_val_if_fail (DZL_IS_MENU_MANAGER (self), NULL); + g_return_val_if_fail (menu_id != NULL, NULL); + + menu = g_hash_table_lookup (self->models, menu_id); + + if (menu == NULL) + { + menu = g_menu_new (); + g_hash_table_insert (self->models, g_strdup (menu_id), menu); + } + + return menu; +} + +guint +dzl_menu_manager_add_resource (DzlMenuManager *self, + const gchar *resource, + GError **error) +{ + GtkBuilder *builder; + guint merge_id; + + g_return_val_if_fail (DZL_IS_MENU_MANAGER (self), 0); + g_return_val_if_fail (resource != NULL, 0); + + if (g_str_has_prefix (resource, "resource://")) + resource += strlen ("resource://"); + + builder = gtk_builder_new (); + + if (!gtk_builder_add_from_resource (builder, resource, error)) + { + g_object_unref (builder); + return 0; + } + + merge_id = ++self->last_merge_id; + dzl_menu_manager_merge_builder (self, builder, merge_id); + g_object_unref (builder); + + return merge_id; +} diff --git a/src/menus/dzl-menu-manager.h b/src/menus/dzl-menu-manager.h new file mode 100644 index 0000000..b91f4e9 --- /dev/null +++ b/src/menus/dzl-menu-manager.h @@ -0,0 +1,56 @@ +/* dzl-menu-manager.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_MENU_MANAGER_H +#define DZL_MENU_MANAGER_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_MENU_MANAGER (dzl_menu_manager_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlMenuManager, dzl_menu_manager, DZL, MENU_MANAGER, GObject) + +DZL_AVAILABLE_IN_ALL +DzlMenuManager *dzl_menu_manager_new (void); +DZL_AVAILABLE_IN_ALL +guint dzl_menu_manager_add_filename (DzlMenuManager *self, + const gchar *filename, + GError **error); +DZL_AVAILABLE_IN_ALL +guint dzl_menu_manager_add_resource (DzlMenuManager *self, + const gchar *resource, + GError **error); +DZL_AVAILABLE_IN_ALL +guint dzl_menu_manager_merge (DzlMenuManager *self, + const gchar *menu_id, + GMenuModel *model); +DZL_AVAILABLE_IN_ALL +void dzl_menu_manager_remove (DzlMenuManager *self, + guint merge_id); +DZL_AVAILABLE_IN_ALL +GMenu *dzl_menu_manager_get_menu_by_id (DzlMenuManager *self, + const gchar *menu_id); + +G_END_DECLS + +#endif /* DZL_MENU_MANAGER_H */ diff --git a/src/menus/meson.build b/src/menus/meson.build new file mode 100644 index 0000000..d918b0e --- /dev/null +++ b/src/menus/meson.build @@ -0,0 +1,22 @@ +menus_headers = [ + 'dzl-joined-menu.h', + 'dzl-menu-button.h', + 'dzl-menu-manager.h', +] + +menus_sources = [ + 'dzl-joined-menu.c', + 'dzl-menu-button.c', + 'dzl-menu-manager.c', +] + +menus_private_sources = [ + 'dzl-menu-button-item.c', + 'dzl-menu-button-section.c', +] + +libdazzle_public_headers += files(menus_headers) +libdazzle_public_sources += files(menus_sources) +libdazzle_private_sources += files(menus_private_sources) + +install_headers(menus_headers, subdir: join_paths(libdazzle_header_subdir, 'menus')) diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..ce750cf --- /dev/null +++ b/src/meson.build @@ -0,0 +1,161 @@ + +libdazzle_header_subdir = join_paths(package_subdir, 'libdazzle-' + apiversion) +libdazzle_header_dir = join_paths(get_option('includedir'), libdazzle_header_subdir) + +libdazzle_resources = gnome.compile_resources( + 'dzl-resources', + 'dazzle.gresources.xml', + + c_name: 'dzl', +) + +dzl_enum_headers = [] + +version_data = configuration_data() +version_data.set('DZL_MAJOR_VERSION', dazzle_version_major) +version_data.set('DZL_MINOR_VERSION', dazzle_version_minor) +version_data.set('DZL_MICRO_VERSION', dazzle_version_micro) +version_data.set('DZL_VERSION', meson.project_version()) + +dzl_version_h = configure_file( + input: 'dzl-version.h.in', + output: 'dzl-version.h', + install_dir: libdazzle_header_dir, + install: true, + configuration: version_data) + +debug_data = configuration_data() +debug_data.set10('ENABLE_TRACING', get_option('enable_tracing')) + +dzl_debug_h = configure_file( + input: 'dzl-debug.h.in', + output: 'dzl-debug.h', + configuration: debug_data) + +libdazzle_generated_headers = [ + dzl_debug_h, + dzl_version_h, +] + +install_headers(['dazzle.h', 'dzl-version-macros.h'], + subdir: libdazzle_header_subdir) + +# Filled out in the subdirs +libdazzle_public_headers = [] +libdazzle_public_sources = [] +libdazzle_private_sources = [] + +subdir('actions') +subdir('animation') +subdir('app') +subdir('bindings') +subdir('cache') +subdir('files') +subdir('graphing') +subdir('menus') +subdir('panel') +subdir('pathbar') +subdir('prefs') +subdir('search') +subdir('settings') +subdir('shortcuts') +subdir('statemachine') +subdir('suggestions') +subdir('theming') +subdir('tree') +subdir('util') +subdir('widgets') + +dzl_enums = gnome.mkenums('dzl-enums', + h_template: 'dzl-enums.h.in', + c_template: 'dzl-enums.c.in', + sources: dzl_enum_headers, + install_header: true, + install_dir: libdazzle_header_dir, +) +libdazzle_public_sources += [dzl_enums[0]] +libdazzle_generated_headers += [dzl_enums[1]] + +libdazzle_sources = [ + libdazzle_generated_headers, + libdazzle_public_sources, + libdazzle_private_sources, + libdazzle_resources, +] + +libdazzle_deps = [ + dependency('gio-2.0', version: '>=2.56.0'), + dependency('gmodule-2.0'), + dependency('gtk+-3.0'), + cc.find_library('m', required: false), + cc.find_library('rt', required: false), +] + +libdazzle_args = [] +libdazzle_args += hidden_visibility_args +if get_option('enable_rdtscp') + libdazzle_args += '-DDZL_HAVE_RDTSCP' +endif + +libdazzle = shared_library( + 'dazzle-' + apiversion, + libdazzle_sources, + + soversion: 0, + c_args: libdazzle_args, + dependencies: libdazzle_deps, + include_directories: [ root_inc, src_inc ], + install: true, +) + +libdazzle_dep = declare_dependency( + sources: libdazzle_generated_headers, + dependencies: libdazzle_deps, + link_with: libdazzle, + include_directories: include_directories('.'), +) + +libdazzle_package = 'libdazzle-@0@'.format(apiversion) + +if get_option('with_introspection') + + libdazzle_gir = gnome.generate_gir(libdazzle, + sources: libdazzle_generated_headers + libdazzle_public_headers + libdazzle_public_sources, + nsversion: apiversion, + namespace: 'Dazzle', + symbol_prefix: 'dzl', + identifier_prefix: 'Dzl', + link_with: libdazzle, + includes: ['Gio-2.0', 'Gtk-3.0'], + install: true, + install_dir_gir: girdir, + install_dir_typelib: typelibdir, + export_packages: libdazzle_package, + extra_args: [ '--c-include=dazzle.h', '--quiet' ], + ) + + if get_option('with_vapi') + + libdazzle_vapi = gnome.generate_vapi('libdazzle-' + apiversion, + sources: libdazzle_gir[0], + packages: [ 'gio-2.0', 'gtk+-3.0' ], + install: true, + install_dir: vapidir, + metadata_dirs: [ meson.current_source_dir() ], + ) + + endif +endif + +pkgg = import('pkgconfig') + +pkgg.generate( + libraries: [libdazzle], + subdirs: libdazzle_header_subdir, + version: meson.project_version(), + name: 'Dazzle', + filebase: libdazzle_package, + description: 'Razzle Dazzle for Gtk+ 3.x applications', + requires: 'gtk+-3.0', + install_dir: join_paths(libdir, 'pkgconfig'), +) diff --git a/src/panel/dzl-dock-bin-edge-private.h b/src/panel/dzl-dock-bin-edge-private.h new file mode 100644 index 0000000..eefe4e8 --- /dev/null +++ b/src/panel/dzl-dock-bin-edge-private.h @@ -0,0 +1,35 @@ +/* dzl-dock-bin-edge-private.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_BIN_EDGE_PRIVATE_H +#define DZL_DOCK_BIN_EDGE_PRIVATE_H + +#include "dzl-dock-bin-edge.h" + +G_BEGIN_DECLS + +void dzl_dock_bin_edge_set_edge (DzlDockBinEdge *self, + GtkPositionType bin_edge); + +G_END_DECLS + +#endif /* DZL_DOCK_BIN_EDGE_PRIVATE_H */ diff --git a/src/panel/dzl-dock-bin-edge.c b/src/panel/dzl-dock-bin-edge.c new file mode 100644 index 0000000..8f3b760 --- /dev/null +++ b/src/panel/dzl-dock-bin-edge.c @@ -0,0 +1,290 @@ +/* dzl-dock-bin-edge.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock-bin-edge" + +#include "config.h" + +#include "panel/dzl-dock-bin.h" +#include "panel/dzl-dock-bin-edge.h" +#include "panel/dzl-dock-bin-edge-private.h" +#include "panel/dzl-dock-item.h" +#include "panel/dzl-dock-revealer.h" +#include "util/dzl-gtk.h" + +typedef struct +{ + GtkPositionType edge : 3; +} DzlDockBinEdgePrivate; + +static void dock_item_iface_init (DzlDockItemInterface *iface); + +G_DEFINE_TYPE_EXTENDED (DzlDockBinEdge, dzl_dock_bin_edge, DZL_TYPE_DOCK_REVEALER, 0, + G_ADD_PRIVATE (DzlDockBinEdge) + G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK_ITEM, dock_item_iface_init)) + +enum { + PROP_0, + PROP_EDGE, + N_PROPS +}; + +enum { + MOVE_TO_BIN_CHILD, + N_SIGNALS +}; + +static GParamSpec *properties [N_PROPS]; +static guint signals [N_SIGNALS]; + +static void +dzl_dock_bin_edge_update_edge (DzlDockBinEdge *self) +{ + DzlDockBinEdgePrivate *priv = dzl_dock_bin_edge_get_instance_private (self); + GtkStyleContext *style_context; + DzlDockRevealerTransitionType transition_type; + const gchar *class_name = NULL; + GtkWidget *child; + GtkOrientation orientation; + + g_assert (DZL_IS_DOCK_BIN_EDGE (self)); + + style_context = gtk_widget_get_style_context (GTK_WIDGET (self)); + + gtk_style_context_remove_class (style_context, "left"); + gtk_style_context_remove_class (style_context, "right"); + gtk_style_context_remove_class (style_context, "top"); + gtk_style_context_remove_class (style_context, "bottom"); + + if (priv->edge == GTK_POS_LEFT) + { + class_name = "left"; + transition_type = DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT; + orientation = GTK_ORIENTATION_VERTICAL; + } + else if (priv->edge == GTK_POS_RIGHT) + { + class_name = "right"; + transition_type = DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT; + orientation = GTK_ORIENTATION_VERTICAL; + } + else if (priv->edge == GTK_POS_TOP) + { + class_name = "top"; + transition_type = DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN; + orientation = GTK_ORIENTATION_HORIZONTAL; + } + else if (priv->edge == GTK_POS_BOTTOM) + { + class_name = "bottom"; + transition_type = DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_UP; + orientation = GTK_ORIENTATION_HORIZONTAL; + } + else + { + g_assert_not_reached (); + return; + } + + gtk_style_context_add_class (style_context, class_name); + dzl_dock_revealer_set_transition_type (DZL_DOCK_REVEALER (self), transition_type); + + child = gtk_bin_get_child (GTK_BIN (self)); + + if (DZL_IS_DOCK_PANED (child)) + gtk_orientable_set_orientation (GTK_ORIENTABLE (child), orientation); +} + +GtkPositionType +dzl_dock_bin_edge_get_edge (DzlDockBinEdge *self) +{ + DzlDockBinEdgePrivate *priv = dzl_dock_bin_edge_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_BIN_EDGE (self), 0); + + return priv->edge; +} + +void +dzl_dock_bin_edge_set_edge (DzlDockBinEdge *self, + GtkPositionType edge) +{ + DzlDockBinEdgePrivate *priv = dzl_dock_bin_edge_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_BIN_EDGE (self)); + + if (edge != priv->edge) + { + priv->edge = edge; + dzl_dock_bin_edge_update_edge (self); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EDGE]); + } +} + +static void +dzl_dock_bin_edge_add (GtkContainer *container, + GtkWidget *widget) +{ + GtkWidget *child; + + g_assert (GTK_IS_CONTAINER (container)); + g_assert (GTK_IS_WIDGET (widget)); + + child = gtk_bin_get_child (GTK_BIN (container)); + gtk_container_add (GTK_CONTAINER (child), widget); + + if (DZL_IS_DOCK_ITEM (child)) + dzl_dock_item_adopt (DZL_DOCK_ITEM (container), DZL_DOCK_ITEM (child)); + + gtk_widget_show (child); +} + +static void +dzl_dock_bin_edge_real_move_to_bin_child (DzlDockBinEdge *self) +{ + GtkWidget *parent; + + g_assert (DZL_IS_DOCK_BIN_EDGE (self)); + + parent = gtk_widget_get_parent (GTK_WIDGET (self)); + + if (DZL_IS_DOCK_BIN (parent)) + gtk_widget_grab_focus (parent); +} + +static void +dzl_dock_bin_edge_constructed (GObject *object) +{ + DzlDockBinEdge *self = (DzlDockBinEdge *)object; + + G_OBJECT_CLASS (dzl_dock_bin_edge_parent_class)->constructed (object); + + dzl_dock_bin_edge_update_edge (self); +} + +static void +dzl_dock_bin_edge_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlDockBinEdge *self = DZL_DOCK_BIN_EDGE (object); + + switch (prop_id) + { + case PROP_EDGE: + g_value_set_enum (value, dzl_dock_bin_edge_get_edge (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_bin_edge_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlDockBinEdge *self = DZL_DOCK_BIN_EDGE (object); + + switch (prop_id) + { + case PROP_EDGE: + dzl_dock_bin_edge_set_edge (self, g_value_get_enum (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_bin_edge_class_init (DzlDockBinEdgeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + GtkBindingSet *binding_set; + + object_class->constructed = dzl_dock_bin_edge_constructed; + object_class->get_property = dzl_dock_bin_edge_get_property; + object_class->set_property = dzl_dock_bin_edge_set_property; + + container_class->add = dzl_dock_bin_edge_add; + + klass->move_to_bin_child = dzl_dock_bin_edge_real_move_to_bin_child; + + properties [PROP_EDGE] = + g_param_spec_enum ("edge", + "Edge", + "The edge of the dock this widget is attached to", + GTK_TYPE_POSITION_TYPE, + GTK_POS_LEFT, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + signals [MOVE_TO_BIN_CHILD] = + g_signal_new ("move-to-bin-child", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (DzlDockBinEdgeClass, move_to_bin_child), + NULL, NULL, NULL, G_TYPE_NONE, 0); + + binding_set = gtk_binding_set_by_class (klass); + gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "move-to-bin-child", 0); + + gtk_widget_class_set_css_name (widget_class, "dzldockbinedge"); +} + +static void +dzl_dock_bin_edge_init (DzlDockBinEdge *self) +{ + GtkWidget *child; + + dzl_gtk_widget_add_style_class (GTK_WIDGET (self), "dzldockbinedge"); + + child = g_object_new (DZL_TYPE_DOCK_PANED, + "visible", TRUE, + NULL); + GTK_CONTAINER_CLASS (dzl_dock_bin_edge_parent_class)->add (GTK_CONTAINER (self), child); +} + +static void +dzl_dock_bin_edge_update_visibility (DzlDockItem *item) +{ + DzlDockBinEdge *self = (DzlDockBinEdge *)item; + GtkWidget *child; + gboolean visible = FALSE; + + g_assert (DZL_IS_DOCK_BIN_EDGE (self)); + + if (NULL != (child = gtk_bin_get_child (GTK_BIN (self)))) + visible = dzl_dock_item_has_widgets (DZL_DOCK_ITEM (child)); + + if (visible != dzl_dock_revealer_get_reveal_child (DZL_DOCK_REVEALER (self))) + dzl_dock_revealer_set_reveal_child (DZL_DOCK_REVEALER (self), visible); +} + +static void +dock_item_iface_init (DzlDockItemInterface *iface) +{ + iface->update_visibility = dzl_dock_bin_edge_update_visibility; +} diff --git a/src/panel/dzl-dock-bin-edge.h b/src/panel/dzl-dock-bin-edge.h new file mode 100644 index 0000000..1234cd4 --- /dev/null +++ b/src/panel/dzl-dock-bin-edge.h @@ -0,0 +1,53 @@ +/* dzl-dock-bin-edge.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_BIN_EDGE_H +#define DZL_DOCK_BIN_EDGE_H + +#include "dzl-version-macros.h" + +#include "dzl-dock-types.h" + +G_BEGIN_DECLS + +struct _DzlDockBinEdgeClass +{ + DzlDockRevealerClass parent; + + void (*move_to_bin_child) (DzlDockBinEdge *self); + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +GtkPositionType dzl_dock_bin_edge_get_edge (DzlDockBinEdge *self); + +G_END_DECLS + +#endif /* DZL_DOCK_BIN_EDGE_H */ diff --git a/src/panel/dzl-dock-bin.c b/src/panel/dzl-dock-bin.c new file mode 100644 index 0000000..d79ba2b --- /dev/null +++ b/src/panel/dzl-dock-bin.c @@ -0,0 +1,2235 @@ +/* dzl-dock-bin.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock-bin" + +#include "config.h" + +#include + +#include "actions/dzl-child-property-action.h" +#include "panel/dzl-dock-bin.h" +#include "panel/dzl-dock-bin-edge-private.h" +#include "panel/dzl-dock-item.h" + +#define HANDLE_WIDTH 3 +#define HANDLE_HEIGHT 3 + +typedef enum +{ + DZL_DOCK_BIN_CHILD_LEFT = GTK_POS_LEFT, + DZL_DOCK_BIN_CHILD_RIGHT = GTK_POS_RIGHT, + DZL_DOCK_BIN_CHILD_TOP = GTK_POS_TOP, + DZL_DOCK_BIN_CHILD_BOTTOM = GTK_POS_BOTTOM, + DZL_DOCK_BIN_CHILD_CENTER = 4, + LAST_DZL_DOCK_BIN_CHILD = 5 +} DzlDockBinChildType; + +typedef struct +{ + /* + * The child widget in question. + * Typically this is a DzlDockBinEdge, but the + * center widget can be whatever. + */ + GtkWidget *widget; + + /* + * The GdkWindow for the handle to resize the edge. + * This is an input only window, the pane handle is drawn + * with CSS by whatever styling the application has chose. + */ + GdkWindow *handle; + + /* + * When dragging, we need to know our offset relative to the + * grab position to alter preferred size requests. + */ + gint drag_offset; + + /* + * This is the position of the child before the drag started. + * We use this, combined with @drag_offset to determine the + * size the child should be in the drag operation. + */ + gint drag_begin_position; + + /* + * Priority child property used to alter which child is + * dominant in each slice stage. See + * dzl_dock_bin_get_children_preferred_width() for more information + * on how the slicing is performed. + */ + gint priority; + + /* + * Cached size request used during size allocation. + */ + GtkRequisition min_req; + GtkRequisition nat_req; + + /* + * The type of child. The DZL_DOCK_BIN_CHILD_CENTER is always + * the last child, and our sort function ensures that. + */ + DzlDockBinChildType type : 3; + + /* + * If the panel is pinned, this will be set to TRUE. A pinned panel + * means that it is displayed juxtapose the center child, where as + * an unpinned child is floating above teh center child. + */ + guint pinned : 1; + + /* + * Tracks if the widget was pinned before the start of an animation + * sequence, as we may change the pinned state during the animation. + */ + guint pre_anim_pinned : 1; +} DzlDockBinChild; + +typedef struct +{ + /* + * All of our dock children, including edges and center child. + */ + DzlDockBinChild children[LAST_DZL_DOCK_BIN_CHILD]; + + /* + * Actions used to toggle edge visibility. + */ + GSimpleActionGroup *actions; + + /* + * The pan gesture is used to resize edges. + */ + GtkGesturePan *pan_gesture; + + /* + * While in a pan gesture, we need to drag the current edge + * being dragged. This is left, right, top, or bottom only. + */ + DzlDockBinChild *drag_child; + + /* + * We need to track the position during a DnD request. We can use this + * to highlight the area where the drop will occur. + */ + gint dnd_drag_x; + gint dnd_drag_y; +} DzlDockBinPrivate; + +static void dzl_dock_bin_init_buildable_iface (GtkBuildableIface *iface); +static void dzl_dock_bin_init_dock_iface (DzlDockInterface *iface); +static void dzl_dock_bin_init_dock_item_iface (DzlDockItemInterface *iface); +static void dzl_dock_bin_create_edge (DzlDockBin *self, + DzlDockBinChild *child, + DzlDockBinChildType type); + +G_DEFINE_TYPE_EXTENDED (DzlDockBin, dzl_dock_bin, GTK_TYPE_CONTAINER, 0, + G_ADD_PRIVATE (DzlDockBin) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, dzl_dock_bin_init_buildable_iface) + G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK_ITEM, dzl_dock_bin_init_dock_item_iface) + G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK, dzl_dock_bin_init_dock_iface)) + +enum { + PROP_0, + PROP_LEFT_VISIBLE, + PROP_RIGHT_VISIBLE, + PROP_TOP_VISIBLE, + PROP_BOTTOM_VISIBLE, + N_PROPS, + + PROP_MANAGER, +}; + +enum { + CHILD_PROP_0, + CHILD_PROP_PINNED, + CHILD_PROP_POSITION, + CHILD_PROP_PRIORITY, + N_CHILD_PROPS +}; + +static GParamSpec *properties [N_PROPS]; +static GParamSpec *child_properties [N_CHILD_PROPS]; +static const gchar *pinned_names[] = { + "left-pinned", + "right-pinned", + "top-pinned", + "bottom-pinned", + NULL, + NULL +}; +static const gchar *visible_names[] = { + "left-visible", + "right-visible", + "top-visible", + "bottom-visible", + NULL, + NULL +}; + +static DzlDockBinChild * +dzl_dock_bin_get_child_typed (DzlDockBin *self, + DzlDockBinChildType type) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (type >= DZL_DOCK_BIN_CHILD_LEFT); + g_assert (type < LAST_DZL_DOCK_BIN_CHILD); + + for (guint i = 0; i < G_N_ELEMENTS (priv->children); i++) + { + if (priv->children[i].type == type) + return &priv->children[i]; + } + + g_assert_not_reached (); + + return NULL; +} + +static GtkWidget * +get_child_widget (DzlDockBin *self, + DzlDockBinChildType type) +{ + switch (type) + { + case DZL_DOCK_BIN_CHILD_LEFT: + return dzl_dock_bin_get_left_edge (self); + + case DZL_DOCK_BIN_CHILD_RIGHT: + return dzl_dock_bin_get_right_edge (self); + + case DZL_DOCK_BIN_CHILD_TOP: + return dzl_dock_bin_get_top_edge (self); + + case DZL_DOCK_BIN_CHILD_BOTTOM: + return dzl_dock_bin_get_bottom_edge (self); + + case DZL_DOCK_BIN_CHILD_CENTER: + case LAST_DZL_DOCK_BIN_CHILD: + default: + return NULL; + } +} + +static gboolean +get_visible (DzlDockBin *self, + DzlDockBinChildType type) +{ + GtkWidget *child; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (type >= DZL_DOCK_BIN_CHILD_LEFT); + g_assert (type <= DZL_DOCK_BIN_CHILD_BOTTOM); + + child = get_child_widget (self, type); + + return DZL_IS_DOCK_REVEALER (child) && + dzl_dock_revealer_get_reveal_child (DZL_DOCK_REVEALER (child)); +} + +static void +set_visible (DzlDockBin *self, + DzlDockBinChildType type, + gboolean visible) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + GtkWidget *widget; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (type >= DZL_DOCK_BIN_CHILD_LEFT); + g_assert (type <= DZL_DOCK_BIN_CHILD_BOTTOM); + + /* Ensure the panel is created */ + widget = get_child_widget (self, type); + + /* + * When animating, we should set the panel as "unpinned" and then + * swap the pinned status after the animation completes. That will + * allow us to have more smooth animations without having to keep + * updating all that sizing machinery. + */ + + if (DZL_IS_DOCK_REVEALER (widget)) + { + DzlDockBinChild *child = &priv->children[type]; + + if (visible != dzl_dock_revealer_get_reveal_child (DZL_DOCK_REVEALER (widget))) + { + + /* If the widget isn't currently visible, first make it visible but keep + * the reveal-child set as FALSE. That allows us to animate in from a + * hiddent initial state. + */ + if (visible && !gtk_widget_get_visible (widget)) + { + dzl_dock_revealer_set_reveal_child (DZL_DOCK_REVEALER (widget), FALSE); + gtk_widget_show (child->widget); + } + + /* Only stash state if there is not an animation in progress. + * Otherwise we stomp on our previous state. + */ + if (!dzl_dock_revealer_is_animating (DZL_DOCK_REVEALER (widget))) + child->pre_anim_pinned = child->pinned; + + /* + * We only want to "unpin" the panel when we animate out. Generally, + * shrinking widgets can be made fast by re-using their existing + * content, but making things bigger has very large performance + * costs. Expecially with textviews/treeviews. + */ + if (!visible) + child->pinned = FALSE; + + dzl_dock_revealer_set_reveal_child (DZL_DOCK_REVEALER (widget), visible); + + g_object_notify (G_OBJECT (self), visible_names [type]); + gtk_widget_queue_resize (GTK_WIDGET (self)); + } + } +} + +static gint +dzl_dock_bin_child_compare (gconstpointer a, + gconstpointer b) +{ + const DzlDockBinChild *child_a = a; + const DzlDockBinChild *child_b = b; + + if (child_a->type == DZL_DOCK_BIN_CHILD_CENTER) + return 1; + else if (child_b->type == DZL_DOCK_BIN_CHILD_CENTER) + return -1; + + if ((child_a->pinned ^ child_b->pinned) != 0) + return child_a->pinned - child_b->pinned; + + return child_a->priority - child_b->priority; +} + +static void +dzl_dock_bin_resort_children (DzlDockBin *self) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + + g_assert (DZL_IS_DOCK_BIN (self)); + + /* + * Sort the children by priority/pinned status, but do not change + * the position of the DZL_DOCK_BIN_CHILD_CENTER child. It should + * always be at the last position. + */ + + g_qsort_with_data (&priv->children[0], + DZL_DOCK_BIN_CHILD_CENTER, + sizeof (DzlDockBinChild), + (GCompareDataFunc)dzl_dock_bin_child_compare, + NULL); + + gtk_widget_queue_allocate (GTK_WIDGET (self)); +} + +static DzlDockBinChild * +dzl_dock_bin_get_child (DzlDockBin *self, + GtkWidget *widget) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + guint i; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GTK_IS_WIDGET (widget)); + + for (i = 0; i < G_N_ELEMENTS (priv->children); i++) + { + DzlDockBinChild *child = &priv->children [i]; + + if ((GtkWidget *)child->widget == widget) + return child; + } + + g_assert_not_reached (); + + return NULL; +} + +static void +dzl_dock_bin_update_focus_chain (DzlDockBin *self) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + DzlDockBinChild *child; + GList *focus_chain = NULL; + guint i; + + g_assert (DZL_IS_DOCK_BIN (self)); + + for (i = G_N_ELEMENTS (priv->children); i > 0; i--) + { + child = &priv->children [i - 1]; + + if ((child->widget != NULL) && + (child->type != DZL_DOCK_BIN_CHILD_CENTER)) + focus_chain = g_list_prepend (focus_chain, child->widget); + } + + child = dzl_dock_bin_get_child_typed (self, DZL_DOCK_BIN_CHILD_CENTER); + + if (child->widget != NULL) + focus_chain = g_list_prepend (focus_chain, child->widget); + + if (focus_chain != NULL) + { + gtk_container_set_focus_chain (GTK_CONTAINER (self), focus_chain); + g_list_free (focus_chain); + } +} + +static void +dzl_dock_bin_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlDockBin *self = (DzlDockBin *)container; + DzlDockBinChild *child; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GTK_IS_WIDGET (widget)); + + child = dzl_dock_bin_get_child_typed (self, DZL_DOCK_BIN_CHILD_CENTER); + + if (child->widget != NULL) + { + g_warning ("Attempt to add a %s to a %s, but it already has a child of type %s", + G_OBJECT_TYPE_NAME (widget), + G_OBJECT_TYPE_NAME (self), + G_OBJECT_TYPE_NAME (child->widget)); + return; + } + + if (DZL_IS_DOCK_ITEM (widget) && + !dzl_dock_item_adopt (DZL_DOCK_ITEM (self), DZL_DOCK_ITEM (widget))) + { + g_warning ("Child of type %s has a different DzlDockManager than %s", + G_OBJECT_TYPE_NAME (widget), G_OBJECT_TYPE_NAME (self)); + return; + } + + child->widget = g_object_ref_sink (widget); + gtk_widget_set_parent (widget, GTK_WIDGET (self)); + + dzl_dock_bin_update_focus_chain (self); + + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static void +dzl_dock_bin_notify_reveal_child (DzlDockBin *self, + GParamSpec *pspec, + GtkWidget *child) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GTK_IS_WIDGET (child)); + + for (guint i = 0; i < G_N_ELEMENTS (priv->children); i++) + { + DzlDockBinChild *ele = &priv->children [i]; + + if (ele->widget == child) + g_object_notify (G_OBJECT (self), visible_names [ele->type]); + } +} + +static void +dzl_dock_bin_notify_child_revealed (DzlDockBin *self, + GParamSpec *pspec, + GtkWidget *child) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GTK_IS_WIDGET (child)); + + for (guint i = 0; i < G_N_ELEMENTS (priv->children); i++) + { + DzlDockBinChild *ele = &priv->children [i]; + + if (ele->widget == child) + { + ele->pinned = ele->pre_anim_pinned; + gtk_widget_queue_resize (GTK_WIDGET (self)); + break; + } + } +} + +static void +dzl_dock_bin_remove (GtkContainer *container, + GtkWidget *widget) +{ + DzlDockBin *self = (DzlDockBin *)container; + DzlDockBinChild *child; + + g_return_if_fail (DZL_IS_DOCK_BIN (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + + child = dzl_dock_bin_get_child (self, widget); + gtk_widget_unparent (child->widget); + g_clear_object (&child->widget); + + g_signal_handlers_disconnect_by_func (widget, + G_CALLBACK (gtk_widget_destroyed), + &child->widget); + g_signal_handlers_disconnect_by_func (widget, + G_CALLBACK (dzl_dock_bin_notify_reveal_child), + self); + g_signal_handlers_disconnect_by_func (widget, + G_CALLBACK (dzl_dock_bin_notify_child_revealed), + self); + + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static void +dzl_dock_bin_forall (GtkContainer *container, + gboolean include_internal, + GtkCallback callback, + gpointer user_data) +{ + DzlDockBin *self = (DzlDockBin *)container; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + DzlDockBinChild *child; + guint i; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (callback != NULL); + + /* + * Always call the "center" child callback first. This helps ensure that + * it is the first child to be rendered. We need that to ensure that panels + * are drawn *above* the center, even when floating. Note that the center + * child is always the last child in the array. + */ + child = &priv->children [G_N_ELEMENTS (priv->children) - 1]; + if (child->widget != NULL) + callback (child->widget, user_data); + + /* + * Normally, we would iterate the array backwards so that we can ensure that + * the list is safe against widget destruction. However, since we have a + * fixed size array, we can walk forwards safely. This helps ensure we + * preserve draw ordering for the various panels when we chain up to the + * container draw vfunc. + */ + + for (i = 0; i < G_N_ELEMENTS (priv->children) - 1; i++) + { + child = &priv->children[i]; + + if (child->widget != NULL) + callback (GTK_WIDGET (child->widget), user_data); + } +} + +static void +dzl_dock_bin_get_children_preferred_width (DzlDockBin *self, + DzlDockBinChild *children, + gint n_children, + gint *min_width, + gint *nat_width) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + DzlDockBinChild *child = children; + gint child_min_width = 0; + gint child_nat_width = 0; + gint neighbor_min_width = 0; + gint neighbor_nat_width = 0; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (children != NULL); + g_assert (n_children > 0); + g_assert (min_width != NULL); + g_assert (nat_width != NULL); + + *min_width = 0; + *nat_width = 0; + + /* + * We have a fairly simple rule for deducing the size request of + * the children layout. Since children edges can have any priority, + * we need to know how to slice them into areas that allow us to + * combine (additive) or negotiate (maximum) widths with the + * neighboring widgets. + * + * . + * . + * +----+---------------------------------+ + * | | 2 | + * | +=================================+..... + * | | | | + * | | | | + * | 1 | 5 | | + * | | | 3 | + * | +==.==.==.==.==.==.==.==.==.=+ | + * | | 4 | | + * +----+----------------------------+----+ + * . . + * . . + * + * The children are sorted in their weighting order. Each child + * will dominate the leftover allocation, in the orientation that + * matters. + * + * 1 and 3 in the diagram above will always be additive with their + * neighbors horizontal neighbors. See the guide does for how this + * gets sliced. Even if 3 were dominant (instead of 2), it would still + * be additive to its neighbors. Same for 1. + * + * Both 2 and 4, will always negotiate their widths with the next + * child. + * + * This allows us to make a fairly simple recursive function to + * size ourselves and then call again with the next child, working our + * way down to 5 (the center widget). + * + * At this point, we walk back up the recursive-stack and do our + * adding or negotiating. + */ + + if (child->widget != NULL) + gtk_widget_get_preferred_width (child->widget, &child_min_width, &child_nat_width); + + if (child == priv->drag_child) + child_nat_width = MAX (child_min_width, + child->drag_begin_position + child->drag_offset); + + if (n_children > 1) + dzl_dock_bin_get_children_preferred_width (self, + &children [1], + n_children - 1, + &neighbor_min_width, + &neighbor_nat_width); + + switch (child->type) + { + case DZL_DOCK_BIN_CHILD_LEFT: + case DZL_DOCK_BIN_CHILD_RIGHT: + if (child->pinned) + { + *min_width = (child_min_width + neighbor_min_width); + *nat_width = (child_nat_width + neighbor_nat_width); + } + else + { + *min_width = MAX (child_min_width, neighbor_min_width); + *nat_width = MAX (child_nat_width, neighbor_nat_width); + } + break; + + case DZL_DOCK_BIN_CHILD_TOP: + case DZL_DOCK_BIN_CHILD_BOTTOM: + *min_width = MAX (child_min_width, neighbor_min_width); + *nat_width = MAX (child_nat_width, neighbor_nat_width); + break; + + case DZL_DOCK_BIN_CHILD_CENTER: + *min_width = child_min_width; + *nat_width = child_min_width; + break; + + case LAST_DZL_DOCK_BIN_CHILD: + default: + g_assert_not_reached (); + } + + child->min_req.width = *min_width; + child->nat_req.width = *nat_width; +} + +static void +dzl_dock_bin_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + DzlDockBin *self = (DzlDockBin *)widget; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (min_width != NULL); + g_assert (nat_width != NULL); + + dzl_dock_bin_get_children_preferred_width (self, + priv->children, + G_N_ELEMENTS (priv->children), + min_width, + nat_width); +} + +static void +dzl_dock_bin_get_children_preferred_height (DzlDockBin *self, + DzlDockBinChild *children, + gint n_children, + gint *min_height, + gint *nat_height) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + DzlDockBinChild *child = children; + gint child_min_height = 0; + gint child_nat_height = 0; + gint neighbor_min_height = 0; + gint neighbor_nat_height = 0; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (children != NULL); + g_assert (n_children > 0); + g_assert (min_height != NULL); + g_assert (nat_height != NULL); + + *min_height = 0; + *nat_height = 0; + + /* + * See dzl_dock_bin_get_children_preferred_width() for more information on + * how this works. This works just like that but the negotiated/additive + * operations are switched between the left/right and top/bottom. + */ + + if (child->widget != NULL) + gtk_widget_get_preferred_height (child->widget, &child_min_height, &child_nat_height); + + if (child == priv->drag_child) + child_nat_height = MAX (child_min_height, + child->drag_begin_position + child->drag_offset); + + if (n_children > 1) + dzl_dock_bin_get_children_preferred_height (self, + &children [1], + n_children - 1, + &neighbor_min_height, + &neighbor_nat_height); + + switch (child->type) + { + case DZL_DOCK_BIN_CHILD_LEFT: + case DZL_DOCK_BIN_CHILD_RIGHT: + *min_height = MAX (child_min_height, neighbor_min_height); + *nat_height = MAX (child_nat_height, neighbor_nat_height); + break; + + case DZL_DOCK_BIN_CHILD_TOP: + case DZL_DOCK_BIN_CHILD_BOTTOM: + if (child->pinned) + { + *min_height = (child_min_height + neighbor_min_height); + *nat_height = (child_nat_height + neighbor_nat_height); + } + else + { + *min_height = MAX (child_min_height, neighbor_min_height); + *nat_height = MAX (child_nat_height, neighbor_nat_height); + } + break; + + case DZL_DOCK_BIN_CHILD_CENTER: + *min_height = child_min_height; + *nat_height = child_min_height; + break; + + case LAST_DZL_DOCK_BIN_CHILD: + default: + g_assert_not_reached (); + } + + child->min_req.height = *min_height; + child->nat_req.height = *nat_height; +} + +static void +dzl_dock_bin_get_preferred_height (GtkWidget *widget, + gint *min_height, + gint *nat_height) +{ + DzlDockBin *self = (DzlDockBin *)widget; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (min_height != NULL); + g_assert (nat_height != NULL); + + dzl_dock_bin_get_children_preferred_height (self, + priv->children, + G_N_ELEMENTS (priv->children), + min_height, + nat_height); +} + +static void +dzl_dock_bin_negotiate_size (DzlDockBin *self, + const GtkAllocation *allocation, + const GtkRequisition *child_min, + const GtkRequisition *child_nat, + const GtkRequisition *neighbor_min, + const GtkRequisition *neighbor_nat, + GtkAllocation *child_alloc) +{ + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (allocation != NULL); + g_assert (child_min != NULL); + g_assert (child_nat != NULL); + g_assert (neighbor_min != NULL); + g_assert (neighbor_nat != NULL); + g_assert (child_alloc != NULL); + + if (allocation->width - child_nat->width < neighbor_min->width) + child_alloc->width = allocation->width - neighbor_min->width; + else + child_alloc->width = child_nat->width; + + if (allocation->height - child_nat->height < neighbor_min->height) + child_alloc->height = allocation->height - neighbor_min->height; + else + child_alloc->height = child_nat->height; +} + +static void +dzl_dock_bin_child_size_allocate (DzlDockBin *self, + DzlDockBinChild *children, + gint n_children, + GtkAllocation *allocation) +{ + DzlDockBinChild *child = children; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (children != NULL); + g_assert (n_children >= 1); + g_assert (allocation != NULL); + + if (n_children == 1) + { + g_assert (child->type == DZL_DOCK_BIN_CHILD_CENTER); + + if (child->widget != NULL && gtk_widget_get_visible (child->widget)) + gtk_widget_size_allocate (child->widget, allocation); + + return; + } + + if (child->widget != NULL && + gtk_widget_get_visible (child->widget) && + gtk_widget_get_child_visible (child->widget)) + { + GtkAllocation child_alloc = { 0 }; + GtkAllocation handle_alloc = { 0 }; + GtkRequisition neighbor_min = { 0 }; + GtkRequisition neighbor_nat = { 0 }; + GtkStyleContext *style_context = gtk_widget_get_style_context (child->widget); + GtkStateFlags state = gtk_style_context_get_state (style_context); + GtkBorder margin; + + gtk_style_context_get_margin (style_context, state, &margin); + + dzl_dock_bin_get_children_preferred_height (self, child, 1, + &child->min_req.height, + &child->nat_req.height); + + dzl_dock_bin_get_children_preferred_width (self, child, 1, + &child->min_req.width, + &child->nat_req.width); + + if (child->pinned && n_children > 1) + { + dzl_dock_bin_get_children_preferred_height (self, + &children [1], + n_children - 1, + &neighbor_min.height, + &neighbor_nat.height); + + dzl_dock_bin_get_children_preferred_width (self, + &children [1], + n_children - 1, + &neighbor_min.width, + &neighbor_nat.width); + } + + dzl_dock_bin_negotiate_size (self, + allocation, + &child->min_req, + &child->nat_req, + &neighbor_min, + &neighbor_nat, + &child_alloc); + + switch (child->type) + { + case DZL_DOCK_BIN_CHILD_LEFT: + child_alloc.x = allocation->x; + child_alloc.y = allocation->y; + child_alloc.height = allocation->height; + + if (child->pinned) + { + allocation->x += child_alloc.width; + allocation->width -= child_alloc.width; + } + + break; + + + case DZL_DOCK_BIN_CHILD_RIGHT: + child_alloc.x = allocation->x + allocation->width - child_alloc.width; + child_alloc.y = allocation->y; + child_alloc.height = allocation->height; + + if (child->pinned) + allocation->width -= child_alloc.width; + + break; + + + case DZL_DOCK_BIN_CHILD_TOP: + child_alloc.x = allocation->x; + child_alloc.y = allocation->y; + child_alloc.width = allocation->width; + + if (child->pinned) + { + allocation->y += child_alloc.height; + allocation->height -= child_alloc.height; + } + + break; + + + case DZL_DOCK_BIN_CHILD_BOTTOM: + child_alloc.x = allocation->x; + child_alloc.y = allocation->y + allocation->height - child_alloc.height; + child_alloc.width = allocation->width; + + if (child->pinned) + allocation->height -= child_alloc.height; + + break; + + + case DZL_DOCK_BIN_CHILD_CENTER: + case LAST_DZL_DOCK_BIN_CHILD: + default: + g_assert_not_reached (); + break; + } + + handle_alloc = child_alloc; + + switch (child->type) + { + case DZL_DOCK_BIN_CHILD_LEFT: + + /* + * When left-to-right, we often have a scrollbar to deal + * with right here. So fudge the allocation position a bit + * to the right so that the scrollbar can still be easily + * hovered. + */ + if (gtk_widget_get_direction (child->widget) == GTK_TEXT_DIR_LTR) + { + handle_alloc.x += handle_alloc.width - (HANDLE_WIDTH / 2); + handle_alloc.width = HANDLE_WIDTH; + } + else + { + handle_alloc.x += handle_alloc.width - HANDLE_WIDTH; + handle_alloc.width = HANDLE_WIDTH; + } + + handle_alloc.x -= margin.right; + + break; + + case DZL_DOCK_BIN_CHILD_RIGHT: + handle_alloc.width = HANDLE_WIDTH; + + if (gtk_widget_get_direction (child->widget) == GTK_TEXT_DIR_RTL) + handle_alloc.x -= (HANDLE_WIDTH / 2); + + handle_alloc.x += margin.left; + + break; + + case DZL_DOCK_BIN_CHILD_BOTTOM: + handle_alloc.height = HANDLE_HEIGHT; + handle_alloc.y += margin.top; + break; + + case DZL_DOCK_BIN_CHILD_TOP: + handle_alloc.y += handle_alloc.height - HANDLE_HEIGHT; + handle_alloc.y -= margin.bottom; + handle_alloc.height = HANDLE_HEIGHT; + break; + + case DZL_DOCK_BIN_CHILD_CENTER: + case LAST_DZL_DOCK_BIN_CHILD: + default: + break; + } + + if (child_alloc.width > 0 && child_alloc.height > 0 && child->handle) + gdk_window_move_resize (child->handle, + handle_alloc.x, handle_alloc.y, + handle_alloc.width, handle_alloc.height); + + gtk_widget_size_allocate (child->widget, &child_alloc); + } + + dzl_dock_bin_child_size_allocate (self, &children [1], n_children - 1, allocation); +} + +static void +dzl_dock_bin_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + DzlDockBin *self = (DzlDockBin *)widget; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + GtkAllocation child_allocation; + guint i; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (allocation != NULL); + + gtk_widget_set_allocation (widget, allocation); + + child_allocation.x = 0; + child_allocation.y = 0; + child_allocation.width = allocation->width; + child_allocation.height = allocation->height; + + if (gtk_widget_get_realized (widget)) + { + gdk_window_move_resize (gtk_widget_get_window (widget), + allocation->x, + allocation->y, + child_allocation.width, + child_allocation.height); + } + + dzl_dock_bin_child_size_allocate (self, + priv->children, + G_N_ELEMENTS (priv->children), + &child_allocation); + + /* + * Hide all of the handle input windows that should be hidden + * because the child has an empty allocation. + */ + + for (i = DZL_DOCK_BIN_CHILD_CENTER; i > 0; i--) + { + DzlDockBinChild *child = &priv->children [i - 1]; + + if (child->handle != NULL) + { + if (DZL_IS_DOCK_BIN_EDGE (child->widget)) + { + if (gtk_widget_get_realized (child->widget)) + gdk_window_raise (gtk_widget_get_window (child->widget)); + + if (dzl_dock_revealer_get_reveal_child (DZL_DOCK_REVEALER (child->widget))) + gdk_window_show (child->handle); + } + else + gdk_window_hide (child->handle); + } + } + + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + +static void +dzl_dock_bin_set_child_pinned (DzlDockBin *self, + GtkWidget *widget, + gboolean pinned) +{ + DzlDockBinChild *child; + GtkStyleContext *style_context; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GTK_IS_WIDGET (widget)); + + child = dzl_dock_bin_get_child (self, widget); + + if (child->type == DZL_DOCK_BIN_CHILD_CENTER) + return; + + child->pinned = !!pinned; + + style_context = gtk_widget_get_style_context (widget); + + if (child->pinned) + gtk_style_context_add_class (style_context, DZL_DOCK_BIN_STYLE_CLASS_PINNED); + else + gtk_style_context_remove_class (style_context, DZL_DOCK_BIN_STYLE_CLASS_PINNED); + + child->pre_anim_pinned = child->pinned; + + dzl_dock_bin_resort_children (self); + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + if (child->widget != NULL) + gtk_container_child_notify_by_pspec (GTK_CONTAINER (self), child->widget, + child_properties [CHILD_PROP_PINNED]); +} + +static void +dzl_dock_bin_set_child_priority (DzlDockBin *self, + GtkWidget *widget, + gint priority) +{ + DzlDockBinChild *child; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GTK_IS_WIDGET (widget)); + + child = dzl_dock_bin_get_child (self, widget); + child->priority = priority; + + dzl_dock_bin_resort_children (self); + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + if (child->widget != NULL) + gtk_container_child_notify_by_pspec (GTK_CONTAINER (self), child->widget, + child_properties [CHILD_PROP_PRIORITY]); +} + +static void +dzl_dock_bin_create_child_handle (DzlDockBin *self, + DzlDockBinChild *child) +{ + GdkWindowAttr attributes = { 0 }; + GdkDisplay *display; + GdkWindow *parent; + const gchar *cursor_name; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (child != NULL); + g_assert (child->type < DZL_DOCK_BIN_CHILD_CENTER); + g_assert (child->handle == NULL); + + display = gtk_widget_get_display (GTK_WIDGET (self)); + parent = gtk_widget_get_window (GTK_WIDGET (self)); + + cursor_name = (child->type == DZL_DOCK_BIN_CHILD_LEFT || child->type == DZL_DOCK_BIN_CHILD_RIGHT) + ? "col-resize" + : "row-resize"; + + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_ONLY; + attributes.x = -1; + attributes.y = -1; + attributes.width = 1; + attributes.height = 1; + attributes.visual = gtk_widget_get_visual (GTK_WIDGET (self)); + attributes.event_mask = (GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_ENTER_NOTIFY_MASK | + GDK_LEAVE_NOTIFY_MASK | + GDK_POINTER_MOTION_MASK); + attributes.cursor = gdk_cursor_new_from_name (display, cursor_name); + + child->handle = gdk_window_new (parent, &attributes, GDK_WA_CURSOR); + gtk_widget_register_window (GTK_WIDGET (self), child->handle); + + g_clear_object (&attributes.cursor); +} + +static void +dzl_dock_bin_destroy_child_handle (DzlDockBin *self, + DzlDockBinChild *child) +{ + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (child != NULL); + g_assert (child->type < DZL_DOCK_BIN_CHILD_CENTER); + + if (child->handle != NULL) + { + gtk_widget_unregister_window (GTK_WIDGET (self), child->handle); + gdk_window_destroy (child->handle); + child->handle = NULL; + } +} + +static void +dzl_dock_bin_realize (GtkWidget *widget) +{ + DzlDockBin *self = (DzlDockBin *)widget; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + GdkWindowAttr attributes = { 0 }; + GdkWindow *parent; + GdkWindow *window; + GtkAllocation alloc; + gint attributes_mask = 0; + guint i; + + g_assert (DZL_IS_DOCK_BIN (self)); + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + gtk_widget_set_realized (GTK_WIDGET (self), TRUE); + + parent = gtk_widget_get_parent_window (GTK_WIDGET (self)); + + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gtk_widget_get_visual (GTK_WIDGET (self)); + attributes.x = alloc.x; + attributes.y = alloc.y; + attributes.width = alloc.width; + attributes.height = alloc.height; + attributes.event_mask = 0; + + attributes_mask = (GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL); + + window = gdk_window_new (parent, &attributes, attributes_mask); + gtk_widget_set_window (GTK_WIDGET (self), window); + gtk_widget_register_window (GTK_WIDGET (self), window); + + for (i = 0; i < DZL_DOCK_BIN_CHILD_CENTER; i++) + { + DzlDockBinChild *child = &priv->children [i]; + + dzl_dock_bin_create_child_handle (self, child); + } +} + +static void +dzl_dock_bin_unrealize (GtkWidget *widget) +{ + DzlDockBin *self = (DzlDockBin *)widget; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + guint i; + + g_assert (DZL_IS_DOCK_BIN (self)); + + for (i = 0; i < DZL_DOCK_BIN_CHILD_CENTER; i++) + { + DzlDockBinChild *child = &priv->children [i]; + + dzl_dock_bin_destroy_child_handle (self, child); + } + + GTK_WIDGET_CLASS (dzl_dock_bin_parent_class)->unrealize (widget); +} + +static void +dzl_dock_bin_map (GtkWidget *widget) +{ + DzlDockBin *self = (DzlDockBin *)widget; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + guint i; + + g_assert (DZL_IS_DOCK_BIN (self)); + + GTK_WIDGET_CLASS (dzl_dock_bin_parent_class)->map (widget); + + for (i = 0; i < DZL_DOCK_BIN_CHILD_CENTER; i++) + { + DzlDockBinChild *child = &priv->children [i]; + + if (child->handle != NULL) + gdk_window_show (child->handle); + } +} + +static void +dzl_dock_bin_unmap (GtkWidget *widget) +{ + DzlDockBin *self = (DzlDockBin *)widget; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + guint i; + + g_assert (DZL_IS_DOCK_BIN (self)); + + for (i = 0; i < DZL_DOCK_BIN_CHILD_CENTER; i++) + { + DzlDockBinChild *child = &priv->children [i]; + + if (child->handle != NULL) + gdk_window_hide (child->handle); + } + + GTK_WIDGET_CLASS (dzl_dock_bin_parent_class)->unmap (widget); +} + +static void +dzl_dock_bin_pan_gesture_drag_begin (DzlDockBin *self, + gdouble x, + gdouble y, + GtkGesturePan *gesture) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + GdkEventSequence *sequence; + DzlDockBinChild *child = NULL; + GtkAllocation child_alloc; + const GdkEvent *event; + guint i; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GTK_IS_GESTURE_PAN (gesture)); + + sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture)); + event = gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence); + + for (i = 0; i < G_N_ELEMENTS (priv->children); i++) + { + if (priv->children [i].handle == event->any.window) + { + child = &priv->children [i]; + break; + } + } + + if (child == NULL || child->type >= DZL_DOCK_BIN_CHILD_CENTER) + { + gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED); + return; + } + + gtk_widget_get_allocation (child->widget, &child_alloc); + + priv->drag_child = child; + priv->drag_child->drag_offset = 0; + + if (child->type == DZL_DOCK_BIN_CHILD_LEFT || child->type == DZL_DOCK_BIN_CHILD_RIGHT) + { + gtk_gesture_pan_set_orientation (gesture, GTK_ORIENTATION_HORIZONTAL); + priv->drag_child->drag_begin_position = child_alloc.width; + } + else + { + gtk_gesture_pan_set_orientation (gesture, GTK_ORIENTATION_VERTICAL); + priv->drag_child->drag_begin_position = child_alloc.height; + } + + gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); +} + +static void +dzl_dock_bin_pan_gesture_drag_end (DzlDockBin *self, + gdouble x, + gdouble y, + GtkGesturePan *gesture) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + GdkEventSequence *sequence; + GtkEventSequenceState state; + GtkAllocation child_alloc; + gint position; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GTK_IS_GESTURE_PAN (gesture)); + + sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture)); + state = gtk_gesture_get_sequence_state (GTK_GESTURE (gesture), sequence); + + if (state == GTK_EVENT_SEQUENCE_DENIED) + goto cleanup; + + g_assert (priv->drag_child != NULL); + g_assert (DZL_IS_DOCK_BIN_EDGE (priv->drag_child->widget)); + + gtk_widget_get_allocation (priv->drag_child->widget, &child_alloc); + + if ((priv->drag_child->type == DZL_DOCK_BIN_CHILD_LEFT) || + (priv->drag_child->type == DZL_DOCK_BIN_CHILD_RIGHT)) + position = child_alloc.width; + else + position = child_alloc.height; + + dzl_dock_revealer_set_position (DZL_DOCK_REVEALER (priv->drag_child->widget), position); + +cleanup: + if (priv->drag_child != NULL) + { + priv->drag_child->drag_offset = 0; + priv->drag_child->drag_begin_position = 0; + priv->drag_child = NULL; + } +} + +static void +dzl_dock_bin_pan_gesture_pan (DzlDockBin *self, + GtkPanDirection direction, + gdouble offset, + GtkGesturePan *gesture) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + gint position; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GTK_IS_GESTURE_PAN (gesture)); + g_assert (priv->drag_child != NULL); + g_assert (priv->drag_child->type < DZL_DOCK_BIN_CHILD_CENTER); + + /* + * This callback is used to adjust the size allocation of the edge in + * question (denoted by priv->drag_child). It is always one of the + * left, right, top, or bottom children. Never the center child. + * + * Because of how GtkRevealer works, we are doing a bit of a workaround + * here. We need the revealer (the DzlDockBinEdge) child to have a size + * request that matches the visible area, otherwise animating out the + * revealer will not look right. + */ + + if (direction == GTK_PAN_DIRECTION_UP) + { + if (priv->drag_child->type == DZL_DOCK_BIN_CHILD_TOP) + offset = -offset; + } + else if (direction == GTK_PAN_DIRECTION_DOWN) + { + if (priv->drag_child->type == DZL_DOCK_BIN_CHILD_BOTTOM) + offset = -offset; + } + else if (direction == GTK_PAN_DIRECTION_LEFT) + { + if (priv->drag_child->type == DZL_DOCK_BIN_CHILD_LEFT) + offset = -offset; + } + else if (direction == GTK_PAN_DIRECTION_RIGHT) + { + if (priv->drag_child->type == DZL_DOCK_BIN_CHILD_RIGHT) + offset = -offset; + } + + priv->drag_child->drag_offset = (gint)offset; + + position = priv->drag_child->drag_offset + priv->drag_child->drag_begin_position; + if (position >= 0) + dzl_dock_revealer_set_position (DZL_DOCK_REVEALER (priv->drag_child->widget), position); +} + +static void +dzl_dock_bin_create_pan_gesture (DzlDockBin *self) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + GtkGesture *gesture; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (priv->pan_gesture == NULL); + + gesture = gtk_gesture_pan_new (GTK_WIDGET (self), GTK_ORIENTATION_HORIZONTAL); + gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (gesture), FALSE); + gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture), GTK_PHASE_CAPTURE); + + g_signal_connect_object (gesture, + "drag-begin", + G_CALLBACK (dzl_dock_bin_pan_gesture_drag_begin), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (gesture, + "drag-end", + G_CALLBACK (dzl_dock_bin_pan_gesture_drag_end), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (gesture, + "pan", + G_CALLBACK (dzl_dock_bin_pan_gesture_pan), + self, + G_CONNECT_SWAPPED); + + priv->pan_gesture = GTK_GESTURE_PAN (gesture); +} + +static void +dzl_dock_bin_drag_enter (DzlDockBin *self, + GdkDragContext *drag_context, + gint x, + gint y, + guint time_) +{ + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GDK_IS_DRAG_CONTEXT (drag_context)); + +} + +static gboolean +dzl_dock_bin_drag_motion (GtkWidget *widget, + GdkDragContext *drag_context, + gint x, + gint y, + guint time_) +{ + DzlDockBin *self = (DzlDockBin *)widget; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GDK_IS_DRAG_CONTEXT (drag_context)); + + /* + * The purpose of this function is to determine of the location for which + * the drag is currently located, is a valid drop site. We first calculate + * the locations for the various zones, and then simply determine which + * zone we are in (or none). + */ + + if (priv->dnd_drag_x == -1 && priv->dnd_drag_y == -1) + dzl_dock_bin_drag_enter (self, drag_context, x, y, time_); + + priv->dnd_drag_x = x; + priv->dnd_drag_y = y; + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + return TRUE; +} + +static void +dzl_dock_bin_drag_leave (GtkWidget *widget, + GdkDragContext *context, + guint time_) +{ + DzlDockBin *self = (DzlDockBin *)widget; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GDK_IS_DRAG_CONTEXT (context)); + + priv->dnd_drag_x = -1; + priv->dnd_drag_y = -1; +} + +static void +dzl_dock_bin_grab_focus (GtkWidget *widget) +{ + DzlDockBin *self = (DzlDockBin *)widget; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + DzlDockBinChild *child; + guint i; + + g_assert (DZL_IS_DOCK_BIN (self)); + + child = dzl_dock_bin_get_child_typed (self, DZL_DOCK_BIN_CHILD_CENTER); + + if (child->widget != NULL) + { + if (gtk_widget_child_focus (child->widget, GTK_DIR_TAB_FORWARD)) + return; + } + + for (i = 0; i < G_N_ELEMENTS (priv->children); i++) + { + child = &priv->children [i]; + + if (DZL_IS_DOCK_REVEALER (child->widget) && + gtk_widget_get_visible (child->widget) && + gtk_widget_get_child_visible (child->widget) && + dzl_dock_revealer_get_reveal_child (DZL_DOCK_REVEALER (child->widget))) + { + if (gtk_widget_child_focus (child->widget, GTK_DIR_TAB_FORWARD)) + return; + } + } +} + +static GtkWidget * +dzl_dock_bin_real_create_edge (DzlDockBin *self, + GtkPositionType edge) +{ + g_assert (DZL_IS_DOCK_BIN (self)); + + return g_object_new (DZL_TYPE_DOCK_BIN_EDGE, + "edge", edge, + "visible", TRUE, + "reveal-child", FALSE, + NULL); +} + +static void +dzl_dock_bin_create_edge (DzlDockBin *self, + DzlDockBinChild *child, + DzlDockBinChildType type) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + g_autoptr(GSimpleActionGroup) map = NULL; + g_autoptr(GAction) pinned = NULL; + g_autoptr(GPropertyAction) visible = NULL; + const gchar *visible_name; + const gchar *pinned_name; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (child != NULL); + g_assert (type >= DZL_DOCK_BIN_CHILD_LEFT); + g_assert (type < DZL_DOCK_BIN_CHILD_CENTER); + + child->widget = DZL_DOCK_BIN_GET_CLASS (self)->create_edge (self, (GtkPositionType)type); + + if (child->widget == NULL) + { + g_warning ("%s failed to create edge widget", + G_OBJECT_TYPE_NAME (self)); + return; + } + else if (!DZL_IS_DOCK_BIN_EDGE (child->widget)) + { + g_warning ("%s child %s is not a DzlDockBinEdge", + G_OBJECT_TYPE_NAME (self), + G_OBJECT_TYPE_NAME (child)); + return; + } + + g_object_set (child->widget, + "edge", (GtkPositionType)type, + "reveal-child", FALSE, + NULL); + + g_signal_connect (child->widget, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &child->widget); + + g_signal_connect_object (child->widget, + "notify::reveal-child", + G_CALLBACK (dzl_dock_bin_notify_reveal_child), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (child->widget, + "notify::child-revealed", + G_CALLBACK (dzl_dock_bin_notify_child_revealed), + self, + G_CONNECT_SWAPPED); + + gtk_widget_set_parent (g_object_ref_sink (child->widget), GTK_WIDGET (self)); + + dzl_dock_item_adopt (DZL_DOCK_ITEM (self), DZL_DOCK_ITEM (child->widget)); + + /* Action for panel children to easily activate */ + map = g_simple_action_group_new (); + pinned = dzl_child_property_action_new ("pinned", GTK_CONTAINER (self), child->widget, "pinned"); + g_action_map_add_action (G_ACTION_MAP (map), pinned); + gtk_widget_insert_action_group (child->widget, "panel", G_ACTION_GROUP (map)); + g_clear_object (&pinned); + + visible_name = visible_names [child->type]; + pinned_name = pinned_names [child->type]; + + /* Add our pinned action */ + pinned = dzl_child_property_action_new (pinned_name, + GTK_CONTAINER (self), + child->widget, + "pinned"); + g_action_map_add_action (G_ACTION_MAP (priv->actions), pinned); + + /* Add our visible action */ + visible = g_property_action_new (visible_name, self, visible_name); + g_action_map_add_action (G_ACTION_MAP (priv->actions), G_ACTION (visible)); + + if (child->pinned) + gtk_style_context_add_class (gtk_widget_get_style_context (child->widget), + DZL_DOCK_BIN_STYLE_CLASS_PINNED); + + g_object_notify (G_OBJECT (self), visible_name); + + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static void +dzl_dock_bin_init_child (DzlDockBin *self, + DzlDockBinChild *child, + DzlDockBinChildType type) +{ + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (child != NULL); + g_assert (type >= DZL_DOCK_BIN_CHILD_LEFT); + g_assert (type < LAST_DZL_DOCK_BIN_CHILD); + + child->type = type; + child->priority = (int)type * 100; + child->pinned = TRUE; + child->pre_anim_pinned = TRUE; +} + +static void +dzl_dock_bin_destroy (GtkWidget *widget) +{ + DzlDockBin *self = DZL_DOCK_BIN (widget); + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + + g_clear_object (&priv->actions); + g_clear_object (&priv->pan_gesture); + + GTK_WIDGET_CLASS (dzl_dock_bin_parent_class)->destroy (widget); +} + +static void +dzl_dock_bin_get_child_property (GtkContainer *container, + GtkWidget *widget, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlDockBin *self = DZL_DOCK_BIN (container); + DzlDockBinChild *child = dzl_dock_bin_get_child (self, widget); + + switch (prop_id) + { + case CHILD_PROP_PRIORITY: + g_value_set_int (value, child->priority); + break; + + case CHILD_PROP_POSITION: + g_value_set_enum (value, (GtkPositionType)child->type); + break; + + case CHILD_PROP_PINNED: + g_value_set_boolean (value, child->pinned); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_dock_bin_set_child_property (GtkContainer *container, + GtkWidget *widget, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlDockBin *self = DZL_DOCK_BIN (container); + + switch (prop_id) + { + case CHILD_PROP_PINNED: + dzl_dock_bin_set_child_pinned (self, widget, g_value_get_boolean (value)); + break; + + case CHILD_PROP_PRIORITY: + dzl_dock_bin_set_child_priority (self, widget, g_value_get_int (value)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_dock_bin_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlDockBin *self = DZL_DOCK_BIN (object); + + switch (prop_id) + { + case PROP_LEFT_VISIBLE: + g_value_set_boolean (value, get_visible (self, DZL_DOCK_BIN_CHILD_LEFT)); + break; + + case PROP_RIGHT_VISIBLE: + g_value_set_boolean (value, get_visible (self, DZL_DOCK_BIN_CHILD_RIGHT)); + break; + + case PROP_TOP_VISIBLE: + g_value_set_boolean (value, get_visible (self, DZL_DOCK_BIN_CHILD_TOP)); + break; + + case PROP_BOTTOM_VISIBLE: + g_value_set_boolean (value, get_visible (self, DZL_DOCK_BIN_CHILD_BOTTOM)); + break; + + case PROP_MANAGER: + g_value_set_object (value, dzl_dock_item_get_manager (DZL_DOCK_ITEM (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_bin_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlDockBin *self = DZL_DOCK_BIN (object); + + switch (prop_id) + { + case PROP_LEFT_VISIBLE: + set_visible (self, DZL_DOCK_BIN_CHILD_LEFT, g_value_get_boolean (value)); + break; + + case PROP_RIGHT_VISIBLE: + set_visible (self, DZL_DOCK_BIN_CHILD_RIGHT, g_value_get_boolean (value)); + break; + + case PROP_TOP_VISIBLE: + set_visible (self, DZL_DOCK_BIN_CHILD_TOP, g_value_get_boolean (value)); + break; + + case PROP_BOTTOM_VISIBLE: + set_visible (self, DZL_DOCK_BIN_CHILD_BOTTOM, g_value_get_boolean (value)); + break; + + case PROP_MANAGER: + dzl_dock_item_set_manager (DZL_DOCK_ITEM (self), g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_bin_class_init (DzlDockBinClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->get_property = dzl_dock_bin_get_property; + object_class->set_property = dzl_dock_bin_set_property; + + widget_class->destroy = dzl_dock_bin_destroy; + widget_class->drag_leave = dzl_dock_bin_drag_leave; + widget_class->drag_motion = dzl_dock_bin_drag_motion; + widget_class->get_preferred_height = dzl_dock_bin_get_preferred_height; + widget_class->get_preferred_width = dzl_dock_bin_get_preferred_width; + widget_class->grab_focus = dzl_dock_bin_grab_focus; + widget_class->map = dzl_dock_bin_map; + widget_class->realize = dzl_dock_bin_realize; + widget_class->size_allocate = dzl_dock_bin_size_allocate; + widget_class->unmap = dzl_dock_bin_unmap; + widget_class->unrealize = dzl_dock_bin_unrealize; + + container_class->add = dzl_dock_bin_add; + container_class->forall = dzl_dock_bin_forall; + container_class->get_child_property = dzl_dock_bin_get_child_property; + container_class->remove = dzl_dock_bin_remove; + container_class->set_child_property = dzl_dock_bin_set_child_property; + + klass->create_edge = dzl_dock_bin_real_create_edge; + + g_object_class_override_property (object_class, PROP_MANAGER, "manager"); + + properties [PROP_LEFT_VISIBLE] = + g_param_spec_boolean ("left-visible", + "Left Visible", + "If the left panel is visible.", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_RIGHT_VISIBLE] = + g_param_spec_boolean ("right-visible", + "Right Visible", + "If the right panel is visible.", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TOP_VISIBLE] = + g_param_spec_boolean ("top-visible", + "Top Visible", + "If the top panel is visible.", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_BOTTOM_VISIBLE] = + g_param_spec_boolean ("bottom-visible", + "Bottom Visible", + "If the bottom panel is visible.", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + child_properties [CHILD_PROP_PINNED] = + g_param_spec_boolean ("pinned", + "Pinned", + "If the child panel is pinned", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + child_properties [CHILD_PROP_POSITION] = + g_param_spec_enum ("position", + "Position", + "The position of the dock edge", + GTK_TYPE_POSITION_TYPE, + GTK_POS_LEFT, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + child_properties [CHILD_PROP_PRIORITY] = + g_param_spec_int ("priority", + "Priority", + "The priority of the dock edge", + G_MININT, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + gtk_container_class_install_child_properties (container_class, N_CHILD_PROPS, child_properties); + + gtk_widget_class_set_css_name (widget_class, "dzldockbin"); +} + +static void +dzl_dock_bin_init (DzlDockBin *self) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + static GtkTargetEntry drag_entries[] = { + { (gchar *)"DZL_DOCK_BIN_WIDGET", GTK_TARGET_SAME_APP, 0 }, + }; + + gtk_widget_set_has_window (GTK_WIDGET (self), TRUE); + + dzl_dock_bin_create_pan_gesture (self); + + gtk_drag_dest_set (GTK_WIDGET (self), + GTK_DEST_DEFAULT_ALL, + drag_entries, + G_N_ELEMENTS (drag_entries), + GDK_ACTION_MOVE); + + priv->dnd_drag_x = -1; + priv->dnd_drag_y = -1; + + dzl_dock_bin_init_child (self, &priv->children [0], DZL_DOCK_BIN_CHILD_LEFT); + dzl_dock_bin_init_child (self, &priv->children [1], DZL_DOCK_BIN_CHILD_RIGHT); + dzl_dock_bin_init_child (self, &priv->children [2], DZL_DOCK_BIN_CHILD_BOTTOM); + dzl_dock_bin_init_child (self, &priv->children [3], DZL_DOCK_BIN_CHILD_TOP); + dzl_dock_bin_init_child (self, &priv->children [4], DZL_DOCK_BIN_CHILD_CENTER); + + priv->actions = g_simple_action_group_new (); + gtk_widget_insert_action_group (GTK_WIDGET (self), "dockbin", G_ACTION_GROUP (priv->actions)); +} + +GtkWidget * +dzl_dock_bin_new (void) +{ + return g_object_new (DZL_TYPE_DOCK_BIN, NULL); +} + +/** + * dzl_dock_bin_get_center_widget: + * @self: A #DzlDockBin + * + * Gets the center widget for the dock. + * + * Returns: (transfer none) (nullable): A #GtkWidget or %NULL. + */ +GtkWidget * +dzl_dock_bin_get_center_widget (DzlDockBin *self) +{ + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + DzlDockBinChild *child; + + g_return_val_if_fail (DZL_IS_DOCK_BIN (self), NULL); + + child = &priv->children [DZL_DOCK_BIN_CHILD_CENTER]; + + return child->widget; +} + +/** + * dzl_dock_bin_get_top_edge: + * Returns: (transfer none): A #GtkWidget + */ +GtkWidget * +dzl_dock_bin_get_top_edge (DzlDockBin *self) +{ + DzlDockBinChild *child; + + g_return_val_if_fail (DZL_IS_DOCK_BIN (self), NULL); + + child = dzl_dock_bin_get_child_typed (self, DZL_DOCK_BIN_CHILD_TOP); + + if (child->widget == NULL) + dzl_dock_bin_create_edge (self, child, DZL_DOCK_BIN_CHILD_TOP); + + return child->widget; +} + +/** + * dzl_dock_bin_get_left_edge: + * Returns: (transfer none): A #GtkWidget + */ +GtkWidget * +dzl_dock_bin_get_left_edge (DzlDockBin *self) +{ + DzlDockBinChild *child; + + g_return_val_if_fail (DZL_IS_DOCK_BIN (self), NULL); + + child = dzl_dock_bin_get_child_typed (self, DZL_DOCK_BIN_CHILD_LEFT); + + if (child->widget == NULL) + dzl_dock_bin_create_edge (self, child, DZL_DOCK_BIN_CHILD_LEFT); + + return child->widget; +} + +/** + * dzl_dock_bin_get_bottom_edge: + * Returns: (transfer none): A #GtkWidget + */ +GtkWidget * +dzl_dock_bin_get_bottom_edge (DzlDockBin *self) +{ + DzlDockBinChild *child; + + g_return_val_if_fail (DZL_IS_DOCK_BIN (self), NULL); + + child = dzl_dock_bin_get_child_typed (self, DZL_DOCK_BIN_CHILD_BOTTOM); + + if (child->widget == NULL) + dzl_dock_bin_create_edge (self, child, DZL_DOCK_BIN_CHILD_BOTTOM); + + return child->widget; +} + +/** + * dzl_dock_bin_get_right_edge: + * Returns: (transfer none): A #GtkWidget + */ +GtkWidget * +dzl_dock_bin_get_right_edge (DzlDockBin *self) +{ + DzlDockBinChild *child; + + g_return_val_if_fail (DZL_IS_DOCK_BIN (self), NULL); + + child = dzl_dock_bin_get_child_typed (self, DZL_DOCK_BIN_CHILD_RIGHT); + + if (child->widget == NULL) + dzl_dock_bin_create_edge (self, child, DZL_DOCK_BIN_CHILD_RIGHT); + + return child->widget; +} + +static void +dzl_dock_bin_init_dock_iface (DzlDockInterface *iface) +{ +} + +static void +dzl_dock_bin_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + DzlDockBin *self = (DzlDockBin *)buildable; + GtkWidget *parent; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GTK_IS_BUILDER (builder)); + g_assert (G_IS_OBJECT (child)); + + if (!GTK_IS_WIDGET (child)) + { + g_warning ("Attempt to add a child of type \"%s\" to a \"%s\"", + G_OBJECT_TYPE_NAME (child), G_OBJECT_TYPE_NAME (self)); + return; + } + + if (DZL_IS_DOCK_ITEM (child) && + !dzl_dock_item_adopt (DZL_DOCK_ITEM (self), DZL_DOCK_ITEM (child))) + { + g_warning ("Child of type %s has a different DzlDockManager than %s", + G_OBJECT_TYPE_NAME (child), G_OBJECT_TYPE_NAME (self)); + return; + } + + if (!type || !*type || (g_strcmp0 ("center", type) == 0)) + { + gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (child)); + return; + } + + if (g_strcmp0 ("top", type) == 0) + parent = dzl_dock_bin_get_top_edge (self); + else if (g_strcmp0 ("bottom", type) == 0) + parent = dzl_dock_bin_get_bottom_edge (self); + else if (g_strcmp0 ("right", type) == 0) + parent = dzl_dock_bin_get_right_edge (self); + else + parent = dzl_dock_bin_get_left_edge (self); + + gtk_container_add (GTK_CONTAINER (parent), GTK_WIDGET (child)); +} + +static GObject * +dzl_dock_bin_get_internal_child (GtkBuildable *buildable, + GtkBuilder *builder, + const gchar *childname) +{ + DzlDockBin *self = (DzlDockBin *)buildable; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (GTK_IS_BUILDER (builder)); + + if (g_strcmp0 ("top", childname) == 0) + return G_OBJECT (dzl_dock_bin_get_top_edge (self)); + else if (g_strcmp0 ("bottom", childname) == 0) + return G_OBJECT (dzl_dock_bin_get_bottom_edge (self)); + else if (g_strcmp0 ("right", childname) == 0) + return G_OBJECT (dzl_dock_bin_get_right_edge (self)); + else if (g_strcmp0 ("left", childname) == 0) + return G_OBJECT (dzl_dock_bin_get_left_edge (self)); + + return NULL; +} + +static void +dzl_dock_bin_init_buildable_iface (GtkBuildableIface *iface) +{ + iface->add_child = dzl_dock_bin_add_child; + iface->get_internal_child = dzl_dock_bin_get_internal_child; +} + +static void +dzl_dock_bin_present_child (DzlDockItem *item, + DzlDockItem *widget) +{ + DzlDockBin *self = (DzlDockBin *)item; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + guint i; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (DZL_IS_DOCK_ITEM (widget)); + g_assert (GTK_IS_WIDGET (widget)); + + for (i = 0; i < G_N_ELEMENTS (priv->children); i++) + { + DzlDockBinChild *child = &priv->children [i]; + + if (DZL_IS_DOCK_BIN_EDGE (child->widget) && + gtk_widget_is_ancestor (GTK_WIDGET (widget), child->widget)) + { + set_visible (self, child->type, TRUE); + return; + } + } +} + +static gboolean +dzl_dock_bin_get_child_visible (DzlDockItem *item, + DzlDockItem *child) +{ + DzlDockBin *self = (DzlDockBin *)item; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + GtkWidget *ancestor; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (DZL_IS_DOCK_ITEM (item)); + + ancestor = gtk_widget_get_ancestor (GTK_WIDGET (child), DZL_TYPE_DOCK_BIN_EDGE); + + if (ancestor == NULL) + return FALSE; + + if ((ancestor == priv->children [0].widget) || + (ancestor == priv->children [1].widget) || + (ancestor == priv->children [2].widget) || + (ancestor == priv->children [3].widget)) + return dzl_dock_revealer_get_child_revealed (DZL_DOCK_REVEALER (ancestor)); + + return FALSE; +} + +static void +dzl_dock_bin_set_child_visible (DzlDockItem *item, + DzlDockItem *child, + gboolean child_visible) +{ + DzlDockBin *self = (DzlDockBin *)item; + GtkWidget *ancestor; + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (DZL_IS_DOCK_ITEM (item)); + + ancestor = gtk_widget_get_ancestor (GTK_WIDGET (child), DZL_TYPE_DOCK_BIN_EDGE); + + if (ancestor != NULL) + dzl_dock_revealer_set_reveal_child (DZL_DOCK_REVEALER (ancestor), child_visible); +} + +static gboolean +dzl_dock_bin_minimize (DzlDockItem *item, + DzlDockItem *child, + GtkPositionType *position) +{ + DzlDockBin *self = (DzlDockBin *)item; + DzlDockBinPrivate *priv = dzl_dock_bin_get_instance_private (self); + + g_assert (DZL_IS_DOCK_BIN (self)); + g_assert (DZL_IS_DOCK_ITEM (child)); + g_assert (position != NULL); + + for (guint i = 0; i < LAST_DZL_DOCK_BIN_CHILD; i++) + { + const DzlDockBinChild *info = &priv->children [i]; + + if (info->widget != NULL && gtk_widget_is_ancestor (GTK_WIDGET (child), info->widget)) + { + switch (info->type) + { + case DZL_DOCK_BIN_CHILD_LEFT: + case DZL_DOCK_BIN_CHILD_CENTER: + case LAST_DZL_DOCK_BIN_CHILD: + default: + *position = GTK_POS_LEFT; + break; + + case DZL_DOCK_BIN_CHILD_RIGHT: + *position = GTK_POS_RIGHT; + break; + + case DZL_DOCK_BIN_CHILD_TOP: + *position = GTK_POS_TOP; + break; + + case DZL_DOCK_BIN_CHILD_BOTTOM: + *position = GTK_POS_BOTTOM; + break; + } + + break; + } + } + + return FALSE; +} + +static void +dzl_dock_bin_init_dock_item_iface (DzlDockItemInterface *iface) +{ + iface->present_child = dzl_dock_bin_present_child; + iface->get_child_visible = dzl_dock_bin_get_child_visible; + iface->set_child_visible = dzl_dock_bin_set_child_visible; + iface->minimize = dzl_dock_bin_minimize; +} diff --git a/src/panel/dzl-dock-bin.h b/src/panel/dzl-dock-bin.h new file mode 100644 index 0000000..6fb9aab --- /dev/null +++ b/src/panel/dzl-dock-bin.h @@ -0,0 +1,66 @@ +/* dzl-dock-bin-bin.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_BIN_H +#define DZL_DOCK_BIN_H + +#include "dzl-version-macros.h" + +#include "dzl-dock-types.h" + +G_BEGIN_DECLS + +#define DZL_DOCK_BIN_STYLE_CLASS_PINNED "pinned" + +struct _DzlDockBinClass +{ + GtkContainerClass parent; + + GtkWidget *(*create_edge) (DzlDockBin *self, + GtkPositionType edge); + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_dock_bin_new (void); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_dock_bin_get_center_widget (DzlDockBin *self); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_dock_bin_get_top_edge (DzlDockBin *self); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_dock_bin_get_left_edge (DzlDockBin *self); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_dock_bin_get_bottom_edge (DzlDockBin *self); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_dock_bin_get_right_edge (DzlDockBin *self); + +G_END_DECLS + +#endif /* DZL_DOCK_BIN_H */ diff --git a/src/panel/dzl-dock-item.c b/src/panel/dzl-dock-item.c new file mode 100644 index 0000000..0d783aa --- /dev/null +++ b/src/panel/dzl-dock-item.c @@ -0,0 +1,663 @@ +/* dzl-dock-item.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock-item" + +#include "config.h" + +#include "dzl-dock-item.h" +#include "dzl-dock-manager.h" +#include "dzl-dock-widget.h" + +G_DEFINE_INTERFACE (DzlDockItem, dzl_dock_item, GTK_TYPE_WIDGET) + +/* + * The DzlDockItem is an interface that acts more like a mixin. + * This is mostly out of wanting to preserve widget inheritance + * without having to duplicate all sorts of plumbing. + */ + +enum { + MANAGER_SET, + N_SIGNALS +}; + +static guint signals [N_SIGNALS]; + +static void +dzl_dock_item_real_set_manager (DzlDockItem *self, + DzlDockManager *manager) +{ + DzlDockManager *old_manager; + + g_assert (DZL_IS_DOCK_ITEM (self)); + g_assert (!manager || DZL_IS_DOCK_MANAGER (manager)); + + if (NULL != (old_manager = dzl_dock_item_get_manager (self))) + { + if (DZL_IS_DOCK (self)) + dzl_dock_manager_unregister_dock (old_manager, DZL_DOCK (self)); + } + + if (manager != NULL) + { + g_object_set_data_full (G_OBJECT (self), + "DZL_DOCK_MANAGER", + g_object_ref (manager), + g_object_unref); + if (DZL_IS_DOCK (self)) + dzl_dock_manager_register_dock (manager, DZL_DOCK (self)); + } + else + g_object_set_data (G_OBJECT (self), "DZL_DOCK_MANAGER", NULL); + + g_signal_emit (self, signals [MANAGER_SET], 0, old_manager); +} + +static DzlDockManager * +dzl_dock_item_real_get_manager (DzlDockItem *self) +{ + g_assert (DZL_IS_DOCK_ITEM (self)); + + return g_object_get_data (G_OBJECT (self), "DZL_DOCK_MANAGER"); +} + +static void +dzl_dock_item_real_update_visibility (DzlDockItem *self) +{ +} + +static void +dzl_dock_item_propagate_manager (DzlDockItem *self) +{ + DzlDockManager *manager; + GPtrArray *ar; + guint i; + + g_return_if_fail (DZL_IS_DOCK_ITEM (self)); + + if (!GTK_IS_CONTAINER (self)) + return; + + if (NULL == (manager = dzl_dock_item_get_manager (self))) + return; + + if (NULL == (ar = g_object_get_data (G_OBJECT (self), "DZL_DOCK_ITEM_DESCENDANTS"))) + return; + + for (i = 0; i < ar->len; i++) + { + DzlDockItem *item = g_ptr_array_index (ar, i); + + dzl_dock_item_set_manager (item, manager); + } +} + +static void +dzl_dock_item_real_manager_set (DzlDockItem *self, + DzlDockManager *manager) +{ + g_assert (DZL_IS_DOCK_ITEM (self)); + g_assert (!manager || DZL_IS_DOCK_MANAGER (manager)); + + dzl_dock_item_propagate_manager (self); +} + +static void +dzl_dock_item_real_release (DzlDockItem *self, + DzlDockItem *child) +{ + g_assert (DZL_IS_DOCK_ITEM (self)); + g_assert (DZL_IS_DOCK_ITEM (child)); + + g_warning ("%s does not know how to release child %s", + G_OBJECT_TYPE_NAME (self), + G_OBJECT_TYPE_NAME (child)); +} + +static gboolean +dzl_dock_item_real_can_minimize (DzlDockItem *self, + DzlDockItem *descendant) +{ + return FALSE; +} + +static gboolean +dzl_dock_item_real_get_can_close (DzlDockItem *self) +{ + return FALSE; +} + +static void +dzl_dock_item_default_init (DzlDockItemInterface *iface) +{ + iface->get_manager = dzl_dock_item_real_get_manager; + iface->set_manager = dzl_dock_item_real_set_manager; + iface->manager_set = dzl_dock_item_real_manager_set; + iface->update_visibility = dzl_dock_item_real_update_visibility; + iface->release = dzl_dock_item_real_release; + iface->get_can_close = dzl_dock_item_real_get_can_close; + iface->can_minimize = dzl_dock_item_real_can_minimize; + + signals [MANAGER_SET] = + g_signal_new ("manager-set", + G_TYPE_FROM_INTERFACE (iface), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlDockItemInterface, manager_set), + NULL, NULL, NULL, + G_TYPE_NONE, 1, DZL_TYPE_DOCK_MANAGER); +} + +/** + * dzl_dock_item_get_manager: + * @self: A #DzlDockItem + * + * Gets the dock manager for this dock item. + * + * Returns: (nullable) (transfer none): A #DzlDockmanager. + */ +DzlDockManager * +dzl_dock_item_get_manager (DzlDockItem *self) +{ + g_return_val_if_fail (DZL_IS_DOCK_ITEM (self), NULL); + + return DZL_DOCK_ITEM_GET_IFACE (self)->get_manager (self); +} + +/** + * dzl_dock_item_set_manager: + * @self: A #DzlDockItem + * @manager: (nullable): A #DzlDockManager + * + * Sets the dock manager for this #DzlDockItem. + */ +void +dzl_dock_item_set_manager (DzlDockItem *self, + DzlDockManager *manager) +{ + g_return_if_fail (DZL_IS_DOCK_ITEM (self)); + g_return_if_fail (!manager || DZL_IS_DOCK_MANAGER (manager)); + + DZL_DOCK_ITEM_GET_IFACE (self)->set_manager (self, manager); +} + +void +dzl_dock_item_update_visibility (DzlDockItem *self) +{ + GtkWidget *parent; + + g_return_if_fail (DZL_IS_DOCK_ITEM (self)); + + DZL_DOCK_ITEM_GET_IFACE (self)->update_visibility (self); + + for (parent = gtk_widget_get_parent (GTK_WIDGET (self)); + parent != NULL; + parent = gtk_widget_get_parent (parent)) + { + if (DZL_IS_DOCK_ITEM (parent)) + DZL_DOCK_ITEM_GET_IFACE (parent)->update_visibility (DZL_DOCK_ITEM (parent)); + } +} + +static void +dzl_dock_item_child_weak_notify (gpointer data, + GObject *where_object_was) +{ + DzlDockItem *self = data; + GPtrArray *descendants; + + g_assert (DZL_IS_DOCK_ITEM (self)); + + descendants = g_object_get_data (G_OBJECT (self), "DZL_DOCK_ITEM_DESCENDANTS"); + + if (descendants != NULL) + g_ptr_array_remove (descendants, where_object_was); + + dzl_dock_item_update_visibility (self); +} + +static void +dzl_dock_item_destroy (DzlDockItem *self) +{ + GPtrArray *descendants; + guint i; + + g_assert (DZL_IS_DOCK_ITEM (self)); + + descendants = g_object_get_data (G_OBJECT (self), "DZL_DOCK_ITEM_DESCENDANTS"); + + if (descendants != NULL) + { + for (i = 0; i < descendants->len; i++) + { + DzlDockItem *child = g_ptr_array_index (descendants, i); + + g_object_weak_unref (G_OBJECT (child), + dzl_dock_item_child_weak_notify, + self); + } + + g_object_set_data (G_OBJECT (self), "DZL_DOCK_ITEM_DESCENDANTS", NULL); + g_ptr_array_unref (descendants); + } +} + +static void +dzl_dock_item_track_child (DzlDockItem *self, + DzlDockItem *child) +{ + GPtrArray *descendants; + guint i; + + g_assert (DZL_IS_DOCK_ITEM (self)); + g_assert (DZL_IS_DOCK_ITEM (child)); + + descendants = g_object_get_data (G_OBJECT (self), "DZL_DOCK_ITEM_DESCENDANTS"); + + if (descendants == NULL) + { + descendants = g_ptr_array_new (); + g_object_set_data (G_OBJECT (self), "DZL_DOCK_ITEM_DESCENDANTS", descendants); + g_signal_connect (self, + "destroy", + G_CALLBACK (dzl_dock_item_destroy), + NULL); + } + + for (i = 0; i < descendants->len; i++) + { + DzlDockItem *item = g_ptr_array_index (descendants, i); + + if (item == child) + return; + } + + g_object_weak_ref (G_OBJECT (child), + dzl_dock_item_child_weak_notify, + self); + + g_ptr_array_add (descendants, child); + + dzl_dock_item_update_visibility (child); +} + +gboolean +dzl_dock_item_adopt (DzlDockItem *self, + DzlDockItem *child) +{ + DzlDockManager *manager; + DzlDockManager *child_manager; + + g_return_val_if_fail (DZL_IS_DOCK_ITEM (self), FALSE); + g_return_val_if_fail (DZL_IS_DOCK_ITEM (child), FALSE); + + manager = dzl_dock_item_get_manager (self); + child_manager = dzl_dock_item_get_manager (child); + + if ((child_manager != NULL) && (manager != NULL) && (child_manager != manager)) + return FALSE; + + if (manager != NULL) + dzl_dock_item_set_manager (child, manager); + + dzl_dock_item_track_child (self, child); + + return TRUE; +} + +void +dzl_dock_item_present_child (DzlDockItem *self, + DzlDockItem *child) +{ + g_assert (DZL_IS_DOCK_ITEM (self)); + g_assert (DZL_IS_DOCK_ITEM (child)); + +#if 0 + g_print ("present_child (%s, %s)\n", + G_OBJECT_TYPE_NAME (self), + G_OBJECT_TYPE_NAME (child)); +#endif + + if (DZL_DOCK_ITEM_GET_IFACE (self)->present_child) + DZL_DOCK_ITEM_GET_IFACE (self)->present_child (self, child); +} + +/** + * dzl_dock_item_present: + * @self: A #DzlDockItem + * + * This widget will walk the widget hierarchy to ensure that the + * dock item is visible to the user. + */ +void +dzl_dock_item_present (DzlDockItem *self) +{ + GtkWidget *parent; + + g_return_if_fail (DZL_IS_DOCK_ITEM (self)); + + for (parent = gtk_widget_get_parent (GTK_WIDGET (self)); + parent != NULL; + parent = gtk_widget_get_parent (parent)) + { + if (DZL_IS_DOCK_ITEM (parent)) + { + dzl_dock_item_present_child (DZL_DOCK_ITEM (parent), self); + dzl_dock_item_present (DZL_DOCK_ITEM (parent)); + return; + } + } +} + +gboolean +dzl_dock_item_has_widgets (DzlDockItem *self) +{ + GPtrArray *ar; + + g_return_val_if_fail (DZL_IS_DOCK_ITEM (self), FALSE); + + if (DZL_IS_DOCK_WIDGET (self)) + return TRUE; + + ar = g_object_get_data (G_OBJECT (self), "DZL_DOCK_ITEM_DESCENDANTS"); + + if (ar != NULL) + { + guint i; + + for (i = 0; i < ar->len; i++) + { + DzlDockItem *child = g_ptr_array_index (ar, i); + + if (dzl_dock_item_has_widgets (child)) + return TRUE; + } + } + + return FALSE; +} + +static void +dzl_dock_item_printf_internal (DzlDockItem *self, + GString *str, + guint depth) +{ + GPtrArray *ar; + guint i; + + g_assert (DZL_IS_DOCK_ITEM (self)); + g_assert (str != NULL); + + + for (i = 0; i < depth; i++) + g_string_append_c (str, ' '); + + g_string_append_printf (str, "%s\n", G_OBJECT_TYPE_NAME (self)); + + ++depth; + + ar = g_object_get_data (G_OBJECT (self), "DZL_DOCK_ITEM_DESCENDANTS"); + + if (ar != NULL) + { + for (i = 0; i < ar->len; i++) + dzl_dock_item_printf_internal (g_ptr_array_index (ar, i), str, depth); + } +} + +void +_dzl_dock_item_printf (DzlDockItem *self) +{ + GString *str; + + g_return_if_fail (DZL_IS_DOCK_ITEM (self)); + + str = g_string_new (NULL); + dzl_dock_item_printf_internal (self, str, 0); + g_printerr ("%s", str->str); + g_string_free (str, TRUE); +} + +/** + * dzl_dock_item_get_parent: + * + * Gets the parent #DzlDockItem, or %NULL. + * + * Returns: (transfer none) (nullable): A #DzlDockItem or %NULL. + */ +DzlDockItem * +dzl_dock_item_get_parent (DzlDockItem *self) +{ + GtkWidget *parent; + + g_return_val_if_fail (DZL_IS_DOCK_ITEM (self), NULL); + + for (parent = gtk_widget_get_parent (GTK_WIDGET (self)); + parent != NULL; + parent = gtk_widget_get_parent (parent)) + { + if (DZL_IS_DOCK_ITEM (parent)) + return DZL_DOCK_ITEM (parent); + } + + return NULL; +} + +gboolean +dzl_dock_item_get_child_visible (DzlDockItem *self, + DzlDockItem *child) +{ + g_return_val_if_fail (DZL_IS_DOCK_ITEM (self), FALSE); + g_return_val_if_fail (DZL_IS_DOCK_ITEM (child), FALSE); + + if (DZL_DOCK_ITEM_GET_IFACE (self)->get_child_visible) + return DZL_DOCK_ITEM_GET_IFACE (self)->get_child_visible (self, child); + + return TRUE; +} + +void +dzl_dock_item_set_child_visible (DzlDockItem *self, + DzlDockItem *child, + gboolean child_visible) +{ + g_return_if_fail (DZL_IS_DOCK_ITEM (self)); + g_return_if_fail (DZL_IS_DOCK_ITEM (child)); + + if (DZL_DOCK_ITEM_GET_IFACE (self)->set_child_visible) + DZL_DOCK_ITEM_GET_IFACE (self)->set_child_visible (self, child, child_visible); +} + +/** + * dzl_dock_item_get_can_close: + * @self: a #DzlDockItem + * + * If this dock item can be closed by the user, this virtual function should be + * implemented by the panel and return %TRUE. + * + * Returns: %TRUE if the dock item can be closed by the user, otherwise %FALSE. + */ +gboolean +dzl_dock_item_get_can_close (DzlDockItem *self) +{ + g_return_val_if_fail (DZL_IS_DOCK_ITEM (self), FALSE); + + if (DZL_DOCK_ITEM_GET_IFACE (self)->get_can_close) + return DZL_DOCK_ITEM_GET_IFACE (self)->get_can_close (self); + + return FALSE; +} + +/** + * dzl_dock_item_close: + * @self: a #DzlDockItem + * + * This function will request that the dock item close itself. + * + * Returns: %TRUE if the dock item was closed + */ +gboolean +dzl_dock_item_close (DzlDockItem *self) +{ + g_return_val_if_fail (DZL_IS_DOCK_ITEM (self), FALSE); + + if (dzl_dock_item_get_can_close (self)) + { + if (DZL_DOCK_ITEM_GET_IFACE (self)->close) + return DZL_DOCK_ITEM_GET_IFACE (self)->close (self); + + gtk_widget_destroy (GTK_WIDGET (self)); + + return TRUE; + } + + return FALSE; +} + +/** + * dzl_dock_item_minimize: + * @self: a #DzlDockItem + * @child: A #DzlDockItem that is a child of @self + * @position: (inout): A location for a #GtkPositionType + * + * This requests that @self minimize @child if it knows how. + * + * If not, it should suggest the gravity for @child if it knows how to + * determine that. For example, a #DzlDockBin might know if the widget was part + * of the right panel and therefore may set @position to %GTK_POS_RIGHT. + * + * Returns: %TRUE if @child was minimized. Otherwise %FALSE and @position + * may be updated to a suggested position. + */ +gboolean +dzl_dock_item_minimize (DzlDockItem *self, + DzlDockItem *child, + GtkPositionType *position) +{ + GtkPositionType unused = GTK_POS_LEFT; + + g_return_val_if_fail (DZL_IS_DOCK_ITEM (self), FALSE); + g_return_val_if_fail (DZL_IS_DOCK_ITEM (child), FALSE); + g_return_val_if_fail (self != child, FALSE); + + if (position == NULL) + position = &unused; + + if (DZL_DOCK_ITEM_GET_IFACE (self)->minimize) + return DZL_DOCK_ITEM_GET_IFACE (self)->minimize (self, child, position); + + return FALSE; +} + +/** + * dzl_dock_item_get_title: + * @self: A #DzlDockItem + * + * Gets the title for the #DzlDockItem. + * + * Generally, you want to use a #DzlDockWidget which has a "title" property you + * can set. But this can be helpful for integration of various container + * widgets. + * + * Returns: (transfer full) (nullable): A newly allocated string or %NULL. + */ +gchar * +dzl_dock_item_get_title (DzlDockItem *self) +{ + g_return_val_if_fail (DZL_IS_DOCK_ITEM (self), NULL); + + if (DZL_DOCK_ITEM_GET_IFACE (self)->get_title) + return DZL_DOCK_ITEM_GET_IFACE (self)->get_title (self); + + return NULL; +} + +/** + * dzl_dock_item_get_icon_name: + * @self: A #DzlDockItem + * + * Gets the icon_name for the #DzlDockItem. + * + * Generally, you want to use a #DzlDockWidget which has a "icon-name" property + * you can set. But this can be helpful for integration of various container + * widgets. + * + * Returns: (transfer full) (nullable): A newly allocated string or %NULL. + */ +gchar * +dzl_dock_item_get_icon_name (DzlDockItem *self) +{ + g_return_val_if_fail (DZL_IS_DOCK_ITEM (self), NULL); + + if (DZL_DOCK_ITEM_GET_IFACE (self)->get_icon_name) + return DZL_DOCK_ITEM_GET_IFACE (self)->get_icon_name (self); + + return NULL; +} + +/** + * dzl_dock_item_release: + * @self: A #DzlDockItem + * + * This virtual method should remove @child from @self if the + * dock item knows how to do so. For example, the #DzlDockStack + * will remove @child from it's internal #GtkStack. + * + * After the virtual function has been executed, child tracking + * will be removed so that #DzlDockItem implementations do not + * need to implement themselves. + */ +void +dzl_dock_item_release (DzlDockItem *self, + DzlDockItem *child) +{ + g_return_if_fail (DZL_IS_DOCK_ITEM (self)); + g_return_if_fail (self == dzl_dock_item_get_parent (child)); + + DZL_DOCK_ITEM_GET_IFACE (self)->release (self, child); + + g_object_weak_unref (G_OBJECT (child), + dzl_dock_item_child_weak_notify, + self); + dzl_dock_item_child_weak_notify (self, (GObject *)child); +} + +/** + * dzl_dock_item_get_can_minimize: (virtual can_minimize) + * @self: a #DzlDockItem + * + * Returns: %TRUE if the widget can be minimized. + */ +gboolean +dzl_dock_item_get_can_minimize (DzlDockItem *self) +{ + DzlDockItem *parent; + + g_return_val_if_fail (DZL_IS_DOCK_ITEM (self), FALSE); + + parent = dzl_dock_item_get_parent (self); + + while (parent != NULL) + { + if (DZL_DOCK_ITEM_GET_IFACE (parent)->can_minimize (parent, self)) + return TRUE; + parent = dzl_dock_item_get_parent (parent); + } + + return FALSE; +} diff --git a/src/panel/dzl-dock-item.h b/src/panel/dzl-dock-item.h new file mode 100644 index 0000000..9cdf152 --- /dev/null +++ b/src/panel/dzl-dock-item.h @@ -0,0 +1,109 @@ +/* dzl-dock-item.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_ITEM_H +#define DZL_DOCK_ITEM_H + +#include "dzl-version-macros.h" + +#include "dzl-dock-manager.h" + +G_BEGIN_DECLS + +struct _DzlDockItemInterface +{ + GTypeInterface parent; + + void (*set_manager) (DzlDockItem *self, + DzlDockManager *manager); + DzlDockManager *(*get_manager) (DzlDockItem *self); + void (*manager_set) (DzlDockItem *self, + DzlDockManager *old_manager); + void (*present_child) (DzlDockItem *self, + DzlDockItem *child); + void (*update_visibility) (DzlDockItem *self); + gboolean (*get_child_visible) (DzlDockItem *self, + DzlDockItem *child); + void (*set_child_visible) (DzlDockItem *self, + DzlDockItem *child, + gboolean child_visible); + gchar *(*get_title) (DzlDockItem *self); + gchar *(*get_icon_name) (DzlDockItem *self); + gboolean (*get_can_close) (DzlDockItem *self); + gboolean (*can_minimize) (DzlDockItem *self, + DzlDockItem *descendant); + gboolean (*close) (DzlDockItem *self); + gboolean (*minimize) (DzlDockItem *self, + DzlDockItem *child, + GtkPositionType *position); + void (*release) (DzlDockItem *self, + DzlDockItem *child); +}; + +DZL_AVAILABLE_IN_ALL +DzlDockManager *dzl_dock_item_get_manager (DzlDockItem *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_item_set_manager (DzlDockItem *self, + DzlDockManager *manager); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_item_adopt (DzlDockItem *self, + DzlDockItem *child); +DZL_AVAILABLE_IN_ALL +void dzl_dock_item_present (DzlDockItem *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_item_present_child (DzlDockItem *self, + DzlDockItem *child); +DZL_AVAILABLE_IN_ALL +void dzl_dock_item_update_visibility (DzlDockItem *self); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_item_has_widgets (DzlDockItem *self); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_item_get_child_visible (DzlDockItem *self, + DzlDockItem *child); +DZL_AVAILABLE_IN_ALL +void dzl_dock_item_set_child_visible (DzlDockItem *self, + DzlDockItem *child, + gboolean child_visible); +DZL_AVAILABLE_IN_ALL +DzlDockItem *dzl_dock_item_get_parent (DzlDockItem *self); +DZL_AVAILABLE_IN_ALL +gchar *dzl_dock_item_get_title (DzlDockItem *self); +DZL_AVAILABLE_IN_ALL +gchar *dzl_dock_item_get_icon_name (DzlDockItem *self); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_item_get_can_close (DzlDockItem *self); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_item_get_can_minimize (DzlDockItem *self); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_item_close (DzlDockItem *self); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_item_minimize (DzlDockItem *self, + DzlDockItem *child, + GtkPositionType *position); +DZL_AVAILABLE_IN_ALL +void dzl_dock_item_release (DzlDockItem *self, + DzlDockItem *child); +void _dzl_dock_item_printf (DzlDockItem *self); + +G_END_DECLS + +#endif /* DZL_DOCK_ITEM_H */ diff --git a/src/panel/dzl-dock-manager.c b/src/panel/dzl-dock-manager.c new file mode 100644 index 0000000..6c932e5 --- /dev/null +++ b/src/panel/dzl-dock-manager.c @@ -0,0 +1,404 @@ +/* dzl-dock-manager.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock-manager" + +#include "config.h" + +#include "panel/dzl-dock-manager.h" +#include "panel/dzl-dock-transient-grab.h" +#include "util/dzl-util-private.h" + +typedef struct +{ + GPtrArray *docks; + DzlDockTransientGrab *grab; + GHashTable *queued_focus_by_toplevel; + guint queued_handler; + gint pause_count; +} DzlDockManagerPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlDockManager, dzl_dock_manager, G_TYPE_OBJECT) + +enum { + REGISTER_DOCK, + UNREGISTER_DOCK, + N_SIGNALS +}; + +static guint signals [N_SIGNALS]; + +static void +dzl_dock_manager_do_set_focus (DzlDockManager *self, + GtkWidget *focus, + GtkWidget *toplevel) +{ + DzlDockManagerPrivate *priv = dzl_dock_manager_get_instance_private (self); + g_autoptr(DzlDockTransientGrab) grab = NULL; + GtkWidget *parent; + + g_assert (DZL_IS_DOCK_MANAGER (self)); + g_assert (GTK_IS_WIDGET (focus)); + g_assert (GTK_IS_WIDGET (toplevel)); + + if (priv->pause_count > 0) + return; + + if (priv->grab != NULL) + { + /* + * If the current transient grab contains the new focus widget, + * then there is nothing for us to do now. + */ + if (dzl_dock_transient_grab_is_descendant (priv->grab, focus)) + return; + } + + /* + * If their is a DzlDockItem in the hierarchy, create a new transient grab. + */ + parent = focus; + while (GTK_IS_WIDGET (parent)) + { + if (DZL_IS_DOCK_ITEM (parent)) + { + /* If we reach a DockItem that doesn't have a manager set, + * then we are probably adding the widgetry to the window + * and grabing focus right now would be intrusive. + */ + if (dzl_dock_item_get_manager (DZL_DOCK_ITEM (parent)) == NULL) + return; + + if (grab == NULL) + grab = dzl_dock_transient_grab_new (); + + dzl_dock_transient_grab_add_item (grab, DZL_DOCK_ITEM (parent)); + } + + if (GTK_IS_POPOVER (parent)) + parent = gtk_popover_get_relative_to (GTK_POPOVER (parent)); + else + parent = gtk_widget_get_parent (parent); + } + + /* + * Steal common hierarchy so that we don't hide it when breaking grabs. + * Then release our previous grab. + */ + if (priv->grab != NULL) + { + if (grab != NULL) + dzl_dock_transient_grab_steal_common_ancestors (grab, priv->grab); + dzl_dock_transient_grab_release (priv->grab); + g_clear_object (&priv->grab); + } + + g_assert (priv->grab == NULL); + + /* Start the grab process */ + if (grab != NULL) + { + priv->grab = g_steal_pointer (&grab); + dzl_dock_transient_grab_acquire (priv->grab); + } +} + +static gboolean +do_delayed_focus_update (gpointer user_data) +{ + DzlDockManager *self = user_data; + DzlDockManagerPrivate *priv = dzl_dock_manager_get_instance_private (self); + g_autoptr(GHashTable) hashtable = NULL; + GHashTableIter iter; + GtkWidget *toplevel; + GtkWidget *focus; + + g_assert (DZL_IS_DOCK_MANAGER (self)); + + priv->queued_handler = 0; + + hashtable = g_steal_pointer (&priv->queued_focus_by_toplevel); + g_hash_table_iter_init (&iter, hashtable); + while (g_hash_table_iter_next (&iter, (gpointer *)&toplevel, (gpointer *)&focus)) + dzl_dock_manager_do_set_focus (self, focus, toplevel); + + return G_SOURCE_REMOVE; +} + +static void +dzl_dock_manager_set_focus (DzlDockManager *self, + GtkWidget *focus, + GtkWidget *toplevel) +{ + DzlDockManagerPrivate *priv = dzl_dock_manager_get_instance_private (self); + + g_assert (DZL_IS_DOCK_MANAGER (self)); + g_assert (GTK_IS_WINDOW (toplevel)); + + if (priv->queued_focus_by_toplevel == NULL) + priv->queued_focus_by_toplevel = g_hash_table_new (NULL, NULL); + + /* + * Don't do anything if we get a NULL focus. Instead, wait for the focus + * to be updated with a widget. + */ + if (focus == NULL) + { + g_hash_table_remove (priv->queued_focus_by_toplevel, toplevel); + return; + } + + /* + * If focus is changing, we want to delay this until the end of the main + * loop cycle so that we don't do too much work when rapidly adding widgets + * to the hierarchy, as they may implicitly grab focus. + */ + g_hash_table_insert (priv->queued_focus_by_toplevel, toplevel, focus); + dzl_clear_source (&priv->queued_handler); + priv->queued_handler = gdk_threads_add_idle (do_delayed_focus_update, self); +} + +static void +dzl_dock_manager_hierarchy_changed (DzlDockManager *self, + GtkWidget *old_toplevel, + GtkWidget *widget) +{ + GtkWidget *toplevel; + + g_assert (DZL_IS_DOCK_MANAGER (self)); + g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel)); + g_assert (GTK_IS_WIDGET (widget)); + + if (GTK_IS_WINDOW (old_toplevel)) + g_signal_handlers_disconnect_by_func (old_toplevel, + G_CALLBACK (dzl_dock_manager_set_focus), + self); + + toplevel = gtk_widget_get_toplevel (widget); + + if (GTK_IS_WINDOW (toplevel)) + g_signal_connect_object (toplevel, + "set-focus", + G_CALLBACK (dzl_dock_manager_set_focus), + self, + G_CONNECT_SWAPPED); +} + +static void +dzl_dock_manager_watch_toplevel (DzlDockManager *self, + GtkWidget *widget) +{ + g_assert (DZL_IS_DOCK_MANAGER (self)); + g_assert (GTK_IS_WIDGET (widget)); + + g_signal_connect_object (widget, + "hierarchy-changed", + G_CALLBACK (dzl_dock_manager_hierarchy_changed), + self, + G_CONNECT_SWAPPED); + + dzl_dock_manager_hierarchy_changed (self, NULL, widget); +} + +static void +dzl_dock_manager_weak_notify (gpointer data, + GObject *where_the_object_was) +{ + DzlDockManager *self = data; + DzlDockManagerPrivate *priv = dzl_dock_manager_get_instance_private (self); + + g_assert (DZL_IS_DOCK_MANAGER (self)); + + g_ptr_array_remove (priv->docks, where_the_object_was); +} + +static void +dzl_dock_manager_real_register_dock (DzlDockManager *self, + DzlDock *dock) +{ + DzlDockManagerPrivate *priv = dzl_dock_manager_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_MANAGER (self)); + g_return_if_fail (DZL_IS_DOCK (dock)); + + g_object_weak_ref (G_OBJECT (dock), dzl_dock_manager_weak_notify, self); + g_ptr_array_add (priv->docks, dock); + dzl_dock_manager_watch_toplevel (self, GTK_WIDGET (dock)); +} + +static void +dzl_dock_manager_real_unregister_dock (DzlDockManager *self, + DzlDock *dock) +{ + DzlDockManagerPrivate *priv = dzl_dock_manager_get_instance_private (self); + guint i; + + g_return_if_fail (DZL_IS_DOCK_MANAGER (self)); + g_return_if_fail (DZL_IS_DOCK (dock)); + + for (i = 0; i < priv->docks->len; i++) + { + DzlDock *iter = g_ptr_array_index (priv->docks, i); + + if (iter == dock) + { + g_object_weak_unref (G_OBJECT (dock), dzl_dock_manager_weak_notify, self); + g_ptr_array_remove_index (priv->docks, i); + break; + } + } +} + +static void +dzl_dock_manager_finalize (GObject *object) +{ + DzlDockManager *self = (DzlDockManager *)object; + DzlDockManagerPrivate *priv = dzl_dock_manager_get_instance_private (self); + + g_clear_object (&priv->grab); + + g_clear_pointer (&priv->queued_focus_by_toplevel, g_hash_table_unref); + + if (priv->queued_handler) + { + g_source_remove (priv->queued_handler); + priv->queued_handler = 0; + } + + while (priv->docks->len > 0) + { + DzlDock *dock = g_ptr_array_index (priv->docks, priv->docks->len - 1); + + g_object_weak_unref (G_OBJECT (dock), dzl_dock_manager_weak_notify, self); + g_ptr_array_remove_index (priv->docks, priv->docks->len - 1); + } + + g_clear_pointer (&priv->docks, g_ptr_array_unref); + + G_OBJECT_CLASS (dzl_dock_manager_parent_class)->finalize (object); +} + +static void +dzl_dock_manager_class_init (DzlDockManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_dock_manager_finalize; + + klass->register_dock = dzl_dock_manager_real_register_dock; + klass->unregister_dock = dzl_dock_manager_real_unregister_dock; + + signals [REGISTER_DOCK] = + g_signal_new ("register-dock", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlDockManagerClass, register_dock), + NULL, NULL, NULL, + G_TYPE_NONE, 1, DZL_TYPE_DOCK); + + signals [UNREGISTER_DOCK] = + g_signal_new ("unregister-dock", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlDockManagerClass, unregister_dock), + NULL, NULL, NULL, + G_TYPE_NONE, 1, DZL_TYPE_DOCK); +} + +static void +dzl_dock_manager_init (DzlDockManager *self) +{ + DzlDockManagerPrivate *priv = dzl_dock_manager_get_instance_private (self); + + priv->docks = g_ptr_array_new (); +} + +DzlDockManager * +dzl_dock_manager_new (void) +{ + return g_object_new (DZL_TYPE_DOCK_MANAGER, NULL); +} + +void +dzl_dock_manager_register_dock (DzlDockManager *self, + DzlDock *dock) +{ + g_return_if_fail (DZL_IS_DOCK_MANAGER (self)); + g_return_if_fail (DZL_IS_DOCK (dock)); + + g_signal_emit (self, signals [REGISTER_DOCK], 0, dock); +} + +void +dzl_dock_manager_unregister_dock (DzlDockManager *self, + DzlDock *dock) +{ + g_return_if_fail (DZL_IS_DOCK_MANAGER (self)); + g_return_if_fail (DZL_IS_DOCK (dock)); + + g_signal_emit (self, signals [UNREGISTER_DOCK], 0, dock); +} + +/** + * dzl_dock_manager_pause_grabs: + * @self: a #DzlDockManager + * + * Requests that the transient grab monitoring stop until + * dzl_dock_manager_unpause_grabs() is called. + * + * This might be useful while setting up UI so that you don't focus + * something unexpectedly. + * + * This function may be called multiple times and after an equivalent + * number of calls to dzl_dock_manager_unpause_grabs(), transient + * grab monitoring will continue. + * + * Since: 3.26 + */ +void +dzl_dock_manager_pause_grabs (DzlDockManager *self) +{ + DzlDockManagerPrivate *priv = dzl_dock_manager_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_MANAGER (self)); + g_return_if_fail (priv->pause_count >= 0); + + priv->pause_count++; +} + +/** + * dzl_dock_manager_unpause_grabs: + * @self: a #DzlDockManager + * + * Unpauses a previous call to dzl_dock_manager_pause_grabs(). + * + * Once the pause count returns to zero, transient grab monitoring + * will be restored. + * + * Since: 3.26 + */ +void +dzl_dock_manager_unpause_grabs (DzlDockManager *self) +{ + DzlDockManagerPrivate *priv = dzl_dock_manager_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_MANAGER (self)); + g_return_if_fail (priv->pause_count > 0); + + priv->pause_count--; +} diff --git a/src/panel/dzl-dock-manager.h b/src/panel/dzl-dock-manager.h new file mode 100644 index 0000000..6204bc4 --- /dev/null +++ b/src/panel/dzl-dock-manager.h @@ -0,0 +1,66 @@ +/* dzl-dock-manager.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_MANAGER_H +#define DZL_DOCK_MANAGER_H + +#include "dzl-version-macros.h" + +#include "dzl-dock-types.h" + +G_BEGIN_DECLS + +struct _DzlDockManagerClass +{ + GObjectClass parent; + + void (*register_dock) (DzlDockManager *self, + DzlDock *dock); + void (*unregister_dock) (DzlDockManager *self, + DzlDock *dock); + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +DzlDockManager *dzl_dock_manager_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_dock_manager_register_dock (DzlDockManager *self, + DzlDock *dock); +DZL_AVAILABLE_IN_ALL +void dzl_dock_manager_unregister_dock (DzlDockManager *self, + DzlDock *dock); +DZL_AVAILABLE_IN_ALL +void dzl_dock_manager_pause_grabs (DzlDockManager *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_manager_unpause_grabs (DzlDockManager *self); + +G_END_DECLS + +#endif /* DZL_DOCK_MANAGER_H */ diff --git a/src/panel/dzl-dock-overlay-edge.c b/src/panel/dzl-dock-overlay-edge.c new file mode 100644 index 0000000..a498d3b --- /dev/null +++ b/src/panel/dzl-dock-overlay-edge.c @@ -0,0 +1,291 @@ +/* dzl-dock-overlay-edge.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock-overlay-edge" + +#include "config.h" + +#include "panel/dzl-dock-item.h" +#include "panel/dzl-dock-overlay-edge.h" +#include "panel/dzl-dock-paned.h" +#include "panel/dzl-dock-paned-private.h" +#include "panel/dzl-dock-stack.h" +#include "util/dzl-util-private.h" + +struct _DzlDockOverlayEdge +{ + DzlBin parent; + GtkPositionType edge : 2; + gint position; +}; + +G_DEFINE_TYPE_EXTENDED (DzlDockOverlayEdge, dzl_dock_overlay_edge, DZL_TYPE_BIN, 0, + G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK_ITEM, NULL)) + +enum { + PROP_0, + PROP_EDGE, + PROP_POSITION, + N_PROPS +}; + +enum { + STYLE_PROP_0, + STYLE_PROP_OVERLAP_SIZE, + STYLE_PROP_MNEMONIC_OVERLAP_SIZE, + N_STYLE_PROPS +}; + +static GParamSpec *properties [N_PROPS]; +static GParamSpec *style_properties [N_STYLE_PROPS]; + +static void +dzl_dock_overlay_edge_update_edge (DzlDockOverlayEdge *self) +{ + GtkWidget *child; + GtkPositionType edge; + GtkStyleContext *style_context; + GtkOrientation orientation; + const gchar *style_class; + + g_assert (DZL_IS_DOCK_OVERLAY_EDGE (self)); + + style_context = gtk_widget_get_style_context (GTK_WIDGET (self)); + + gtk_style_context_remove_class (style_context, "left"); + gtk_style_context_remove_class (style_context, "right"); + gtk_style_context_remove_class (style_context, "top"); + gtk_style_context_remove_class (style_context, "bottom"); + + switch (self->edge) + { + case GTK_POS_TOP: + edge = GTK_POS_BOTTOM; + orientation = GTK_ORIENTATION_HORIZONTAL; + style_class = "top"; + break; + + case GTK_POS_BOTTOM: + edge = GTK_POS_TOP; + orientation = GTK_ORIENTATION_HORIZONTAL; + style_class = "bottom"; + break; + + case GTK_POS_LEFT: + edge = GTK_POS_RIGHT; + orientation = GTK_ORIENTATION_VERTICAL; + style_class = "left"; + break; + + case GTK_POS_RIGHT: + edge = GTK_POS_LEFT; + orientation = GTK_ORIENTATION_VERTICAL; + style_class = "right"; + break; + + default: + g_assert_not_reached (); + return; + } + + gtk_style_context_add_class (style_context, style_class); + + child = gtk_bin_get_child (GTK_BIN (self)); + + if (DZL_IS_DOCK_PANED (child)) + { + gtk_orientable_set_orientation (GTK_ORIENTABLE (child), orientation); + dzl_dock_paned_set_child_edge (DZL_DOCK_PANED (child), edge); + } + else if (DZL_IS_DOCK_STACK (child)) + { + dzl_dock_stack_set_edge (DZL_DOCK_STACK (child), edge); + } +} + +static void +dzl_dock_overlay_edge_add (GtkContainer *container, + GtkWidget *child) +{ + DzlDockOverlayEdge *self = (DzlDockOverlayEdge *)container; + + g_assert (DZL_IS_DOCK_OVERLAY_EDGE (self)); + g_assert (GTK_IS_WIDGET (child)); + + GTK_CONTAINER_CLASS (dzl_dock_overlay_edge_parent_class)->add (container, child); + + dzl_dock_overlay_edge_update_edge (self); + + if (DZL_IS_DOCK_ITEM (child)) + dzl_dock_item_adopt (DZL_DOCK_ITEM (self), DZL_DOCK_ITEM (child)); +} + +static void +dzl_dock_overlay_edge_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlDockOverlayEdge *self = DZL_DOCK_OVERLAY_EDGE (object); + + switch (prop_id) + { + case PROP_EDGE: + g_value_set_enum (value, dzl_dock_overlay_edge_get_edge (self)); + break; + + case PROP_POSITION: + g_value_set_int (value, dzl_dock_overlay_edge_get_position (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_overlay_edge_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlDockOverlayEdge *self = DZL_DOCK_OVERLAY_EDGE (object); + + switch (prop_id) + { + case PROP_EDGE: + dzl_dock_overlay_edge_set_edge (self, g_value_get_enum (value)); + break; + + case PROP_POSITION: + dzl_dock_overlay_edge_set_position (self, g_value_get_int (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_overlay_edge_class_init (DzlDockOverlayEdgeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = dzl_dock_overlay_edge_get_property; + object_class->set_property = dzl_dock_overlay_edge_set_property; + + container_class->add = dzl_dock_overlay_edge_add; + + properties [PROP_EDGE] = + g_param_spec_enum ("edge", + "Edge", + "Edge", + GTK_TYPE_POSITION_TYPE, + GTK_POS_LEFT, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_POSITION] = + g_param_spec_int ("position", + "Position", + "The size of the edge", + 0, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + style_properties [STYLE_PROP_MNEMONIC_OVERLAP_SIZE] = + g_param_spec_int ("mnemonic-overlap-size", + "Mnemonic Overlap Size", + "The amount of pixels to overlap when mnemonics are visible", + 0, + G_MAXINT, + 30, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + gtk_widget_class_install_style_property (widget_class, + style_properties [STYLE_PROP_MNEMONIC_OVERLAP_SIZE]); + + style_properties [STYLE_PROP_OVERLAP_SIZE] = + g_param_spec_int ("overlap-size", + "Overlap Size", + "The amount of pixels to overlap when hidden", + 0, + G_MAXINT, + 5, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + gtk_widget_class_install_style_property (widget_class, + style_properties [STYLE_PROP_OVERLAP_SIZE]); + + gtk_widget_class_set_css_name (widget_class, "dzldockoverlayedge"); +} + +static void +dzl_dock_overlay_edge_init (DzlDockOverlayEdge *self) +{ +} + +gint +dzl_dock_overlay_edge_get_position (DzlDockOverlayEdge *self) +{ + g_return_val_if_fail (DZL_IS_DOCK_OVERLAY_EDGE (self), 0); + + return self->position; +} + +void +dzl_dock_overlay_edge_set_position (DzlDockOverlayEdge *self, + gint position) +{ + g_return_if_fail (DZL_IS_DOCK_OVERLAY_EDGE (self)); + g_return_if_fail (position >= 0); + + if (position != self->position) + { + self->position = position; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSITION]); + } +} + +GtkPositionType +dzl_dock_overlay_edge_get_edge (DzlDockOverlayEdge *self) +{ + g_return_val_if_fail (DZL_IS_DOCK_OVERLAY_EDGE (self), 0); + + return self->edge; +} + +void +dzl_dock_overlay_edge_set_edge (DzlDockOverlayEdge *self, + GtkPositionType edge) +{ + g_return_if_fail (DZL_IS_DOCK_OVERLAY_EDGE (self)); + g_return_if_fail (edge >= 0); + g_return_if_fail (edge <= 3); + + if (edge != self->edge) + { + self->edge = edge; + dzl_dock_overlay_edge_update_edge (self); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EDGE]); + } +} diff --git a/src/panel/dzl-dock-overlay-edge.h b/src/panel/dzl-dock-overlay-edge.h new file mode 100644 index 0000000..67c50a7 --- /dev/null +++ b/src/panel/dzl-dock-overlay-edge.h @@ -0,0 +1,45 @@ +/* dzl-dock-overlay-edge.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_OVERLAY_EDGE_H +#define DZL_DOCK_OVERLAY_EDGE_H + +#include "dzl-version-macros.h" + +#include "dzl-dock-types.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +GtkPositionType dzl_dock_overlay_edge_get_edge (DzlDockOverlayEdge *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_overlay_edge_set_edge (DzlDockOverlayEdge *self, + GtkPositionType edge); +DZL_AVAILABLE_IN_ALL +gint dzl_dock_overlay_edge_get_position (DzlDockOverlayEdge *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_overlay_edge_set_position (DzlDockOverlayEdge *self, + gint position); + +G_END_DECLS + +#endif /* DZL_DOCK_OVERLAY_EDGE_H */ diff --git a/src/panel/dzl-dock-overlay.c b/src/panel/dzl-dock-overlay.c new file mode 100644 index 0000000..7d7efa1 --- /dev/null +++ b/src/panel/dzl-dock-overlay.c @@ -0,0 +1,1135 @@ +/* dzl-dock-overlay.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock-overlay" + +#include "config.h" + +#include "animation/dzl-animation.h" +#include "panel/dzl-dock-overlay-edge.h" +#include "panel/dzl-dock-item.h" +#include "panel/dzl-dock-overlay.h" +#include "panel/dzl-tab.h" +#include "panel/dzl-tab-strip.h" +#include "util/dzl-util-private.h" + +#define MNEMONIC_REVEAL_DURATION 200 + +typedef struct +{ + GtkOverlay *overlay; + DzlDockOverlayEdge *edges [4]; + GtkAdjustment *edge_adj [4]; + GtkAdjustment *edge_handle_adj [4]; + GtkAllocation hover_borders [4]; + guint child_reveal : 4; + guint child_revealed : 4; + guint child_transient : 4; +} DzlDockOverlayPrivate; + +static void dzl_dock_overlay_init_dock_iface (DzlDockInterface *iface); +static void dzl_dock_overlay_init_dock_item_iface (DzlDockItemInterface *iface); +static void dzl_dock_overlay_init_buildable_iface (GtkBuildableIface *iface); +static void dzl_dock_overlay_set_child_reveal (DzlDockOverlay *self, + GtkWidget *child, + gboolean reveal); + +G_DEFINE_TYPE_EXTENDED (DzlDockOverlay, dzl_dock_overlay, GTK_TYPE_EVENT_BOX, 0, + G_ADD_PRIVATE (DzlDockOverlay) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, dzl_dock_overlay_init_buildable_iface) + G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK_ITEM, dzl_dock_overlay_init_dock_item_iface) + G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK, dzl_dock_overlay_init_dock_iface)) + +enum { + PROP_0, + PROP_MANAGER, + N_PROPS +}; + +enum { + CHILD_PROP_0, + CHILD_PROP_REVEAL, + CHILD_PROP_REVEALED, + N_CHILD_PROPS +}; + +enum { + HIDE_EDGES, + N_SIGNALS +}; + +static GParamSpec *child_properties [N_CHILD_PROPS]; +static guint signals [N_SIGNALS]; + +static void +dzl_dock_overlay_update_focus_chain (DzlDockOverlay *self) +{ + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + GList *focus_chain = NULL; + GtkWidget *child; + guint i; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + + for (i = G_N_ELEMENTS (priv->edges); i > 0; i--) + { + DzlDockOverlayEdge *edge = priv->edges [i - 1]; + + if (edge != NULL) + focus_chain = g_list_prepend (focus_chain, edge); + } + + child = gtk_bin_get_child (GTK_BIN (self)); + + if (child != NULL) + focus_chain = g_list_prepend (focus_chain, child); + + if (focus_chain != NULL) + { + gtk_container_set_focus_chain (GTK_CONTAINER (self), focus_chain); + g_list_free (focus_chain); + } +} + +static void +dzl_dock_overlay_get_edge_position (DzlDockOverlay *self, + DzlDockOverlayEdge *edge, + GtkAllocation *allocation) +{ + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + GtkPositionType type; + gdouble value; + gdouble handle_value; + gdouble flipped_value; + gint nat_width; + gint nat_height; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (DZL_IS_DOCK_OVERLAY_EDGE (edge)); + g_assert (allocation != NULL); + + gtk_widget_get_allocation (GTK_WIDGET (self), allocation); + + allocation->x = 0; + allocation->y = 0; + + type = dzl_dock_overlay_edge_get_edge (edge); + + if (type == GTK_POS_LEFT || type == GTK_POS_RIGHT) + { + nat_height = MAX (allocation->height, 1); + gtk_widget_get_preferred_width_for_height (GTK_WIDGET (edge), nat_height, NULL, &nat_width); + } + else if (type == GTK_POS_TOP || type == GTK_POS_BOTTOM) + { + nat_width = MAX (allocation->width, 1); + gtk_widget_get_preferred_height_for_width (GTK_WIDGET (edge), nat_width, NULL, &nat_height); + } + else + { + g_assert_not_reached (); + return; + } + + value = gtk_adjustment_get_value (priv->edge_adj [type]); + flipped_value = 1.0 - value; + + handle_value = gtk_adjustment_get_value (priv->edge_handle_adj [type]); + + switch (type) + { + case GTK_POS_LEFT: + allocation->width = nat_width; + allocation->x -= nat_width * value; + if (flipped_value * nat_width <= handle_value) + allocation->x += (handle_value - (flipped_value * nat_width)); + break; + + case GTK_POS_RIGHT: + allocation->x = allocation->x + allocation->width - nat_width; + allocation->width = nat_width; + allocation->x += nat_width * value; + if (flipped_value * nat_width <= handle_value) + allocation->x -= (handle_value - (flipped_value * nat_width)); + break; + + case GTK_POS_BOTTOM: + allocation->y = allocation->y + allocation->height - nat_height; + allocation->height = nat_height; + allocation->y += nat_height * value; + if (flipped_value * nat_height <= handle_value) + allocation->y -= (handle_value - (flipped_value * nat_height)); + break; + + case GTK_POS_TOP: + allocation->height = nat_height; + allocation->y -= nat_height * value; + if (flipped_value * nat_height <= handle_value) + allocation->y += (handle_value - (flipped_value * nat_height)); + break; + + default: + g_assert_not_reached (); + } +} + +static gboolean +dzl_dock_overlay_get_child_position (DzlDockOverlay *self, + GtkWidget *widget, + GtkAllocation *allocation) +{ + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (GTK_IS_WIDGET (widget)); + g_assert (allocation != NULL); + + if (DZL_IS_DOCK_OVERLAY_EDGE (widget)) + { + dzl_dock_overlay_get_edge_position (self, DZL_DOCK_OVERLAY_EDGE (widget), allocation); + return TRUE; + } + + return FALSE; +} + +static void +dzl_dock_overlay_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlDockOverlay *self = (DzlDockOverlay *)container; + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (GTK_IS_WIDGET (widget)); + + gtk_container_add (GTK_CONTAINER (priv->overlay), widget); + + dzl_dock_overlay_update_focus_chain (self); + + if (DZL_IS_DOCK_ITEM (widget)) + { + dzl_dock_item_adopt (DZL_DOCK_ITEM (self), DZL_DOCK_ITEM (widget)); + dzl_dock_item_update_visibility (DZL_DOCK_ITEM (widget)); + } +} + +static void +dzl_dock_overlay_toplevel_mnemonics (DzlDockOverlay *self, + GParamSpec *pspec, + GtkWindow *toplevel) +{ + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + const gchar *style_prop; + gboolean mnemonics_visible; + guint i; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (pspec != NULL); + g_assert (GTK_IS_WINDOW (toplevel)); + + mnemonics_visible = gtk_window_get_mnemonics_visible (toplevel); + style_prop = mnemonics_visible ? "mnemonic-overlap-size" : "overlap-size"; + + for (i = 0; i < G_N_ELEMENTS (priv->edges); i++) + { + DzlDockOverlayEdge *edge = priv->edges [i]; + GtkAdjustment *handle_adj = priv->edge_handle_adj [i]; + gint overlap = 0; + + gtk_widget_style_get (GTK_WIDGET (edge), style_prop, &overlap, NULL); + + dzl_object_animate (handle_adj, + DZL_ANIMATION_EASE_IN_OUT_CUBIC, + MNEMONIC_REVEAL_DURATION, + gtk_widget_get_frame_clock (GTK_WIDGET (self)), + "value", (gdouble)overlap, + NULL); + } + + gtk_widget_queue_allocate (GTK_WIDGET (self)); +} + +typedef struct +{ + DzlDockOverlay *self; + DzlDockOverlayEdge *edge; + GtkWidget *current_grab; + gboolean result; +} ForallState; + +/* Same as gtk_widget_is_ancestor but take care of + * following the popovers relative-to links. + */ +static gboolean +dzl_overlay_dock_widget_is_ancestor (GtkWidget *widget, + GtkWidget *ancestor) +{ + GtkWidget *parent; + + g_assert (GTK_IS_WIDGET (widget)); + g_assert (GTK_IS_WIDGET (ancestor)); + + while (widget != NULL) + { + if (GTK_IS_POPOVER (widget)) + { + if (NULL == (widget = gtk_popover_get_relative_to (GTK_POPOVER (widget)))) + return FALSE; + + if (widget == ancestor) + return TRUE; + } + + parent = gtk_widget_get_parent (widget); + if (parent == ancestor) + return TRUE; + + widget = parent; + } + + return FALSE; +} + +static void +dzl_overlay_container_forall_cb (GtkWidget *widget, + gpointer user_data) +{ + ForallState *state = (ForallState *)user_data; + + if (state->result == TRUE) + return; + + if (GTK_IS_POPOVER (widget) && + gtk_widget_is_visible (widget) && + state->current_grab == widget && + dzl_overlay_dock_widget_is_ancestor (widget, GTK_WIDGET (state->edge))) + state->result = TRUE; +} + +static gboolean +dzl_dock_overlay_edge_need_to_close (DzlDockOverlay *self, + DzlDockOverlayEdge *edge, + GtkWidget *focus) +{ + GtkWidget *toplevel; + GtkWidget *current_grab; + GtkWidget *current_focus; + gboolean result = TRUE; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (DZL_IS_DOCK_OVERLAY_EDGE (edge)); + g_assert (focus == NULL || GTK_IS_WIDGET (focus)); + + if (focus != NULL) + return !dzl_overlay_dock_widget_is_ancestor (focus, GTK_WIDGET (edge)); + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (edge)); + current_grab = gtk_grab_get_current (); + if (current_grab != NULL) + { + if (GTK_IS_WINDOW (toplevel)) + { + ForallState state = {self, edge, current_grab, FALSE}; + + gtk_container_forall (GTK_CONTAINER (toplevel), dzl_overlay_container_forall_cb, &state); + result = !state.result; + } + } + else + { + if (GTK_IS_WINDOW (toplevel) && + NULL != (current_focus = gtk_window_get_focus (GTK_WINDOW (toplevel)))) + result = !dzl_overlay_dock_widget_is_ancestor (current_focus, GTK_WIDGET (edge)); + } + + return result; +} + +static void +dzl_dock_overlay_toplevel_set_focus (DzlDockOverlay *self, + GtkWidget *widget, + GtkWindow *toplevel) +{ + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + guint i; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (!widget || GTK_IS_WIDGET (widget)); + g_assert (GTK_IS_WINDOW (toplevel)); + + /* + * TODO: If the overlay obscurs the new focus widget, + * hide immediately. Otherwise, use a short timeout. + */ + for (i = 0; i < G_N_ELEMENTS (priv->edges); i++) + { + DzlDockOverlayEdge *edge = priv->edges [i]; + + if (!!(priv->child_reveal & (1 << i)) && + dzl_dock_overlay_edge_need_to_close (self, edge, widget)) + { + gtk_container_child_set (GTK_CONTAINER (self), GTK_WIDGET (edge), + "reveal", FALSE, + NULL); + } + } +} + +static void +dzl_dock_overlay_hide_edges (DzlDockOverlay *self) +{ + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + GtkWidget *child; + guint i; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + + for (i = 0; i < G_N_ELEMENTS (priv->edges); i++) + { + DzlDockOverlayEdge *edge = priv->edges [i]; + + gtk_container_child_set (GTK_CONTAINER (self), GTK_WIDGET (edge), + "reveal", FALSE, + NULL); + } + + child = gtk_bin_get_child (GTK_BIN (self)); + + if (child != NULL) + gtk_widget_grab_focus (child); +} + +static void +dzl_dock_overlay_destroy (GtkWidget *widget) +{ + DzlDockOverlay *self = (DzlDockOverlay *)widget; + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + guint i; + + g_assert (GTK_IS_WIDGET (widget)); + + for (i = 0; i < G_N_ELEMENTS (priv->edge_adj); i++) + g_clear_object (&priv->edge_adj [i]); + + GTK_WIDGET_CLASS (dzl_dock_overlay_parent_class)->destroy (widget); +} + +static void +dzl_dock_overlay_hierarchy_changed (GtkWidget *widget, + GtkWidget *old_toplevel) +{ + DzlDockOverlay *self = (DzlDockOverlay *)widget; + GtkWidget *toplevel; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel)); + + if (old_toplevel != NULL) + { + g_signal_handlers_disconnect_by_func (old_toplevel, + G_CALLBACK (dzl_dock_overlay_toplevel_mnemonics), + self); + g_signal_handlers_disconnect_by_func (old_toplevel, + G_CALLBACK (dzl_dock_overlay_toplevel_set_focus), + self); + } + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self)); + + if (GTK_IS_WINDOW (toplevel)) + { + g_signal_connect_object (toplevel, + "notify::mnemonics-visible", + G_CALLBACK (dzl_dock_overlay_toplevel_mnemonics), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (toplevel, + "set-focus", + G_CALLBACK (dzl_dock_overlay_toplevel_set_focus), + self, + G_CONNECT_SWAPPED); + } +} + +static gboolean +dzl_dock_overlay_get_child_reveal (DzlDockOverlay *self, + GtkWidget *child) +{ + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (GTK_IS_WIDGET (child)); + + if (DZL_IS_DOCK_OVERLAY_EDGE (child)) + { + GtkPositionType edge; + + edge = dzl_dock_overlay_edge_get_edge (DZL_DOCK_OVERLAY_EDGE (child)); + + return !!(priv->child_reveal & (1 << edge)); + } + + return FALSE; +} + +static gboolean +dzl_dock_overlay_get_child_revealed (DzlDockOverlay *self, + GtkWidget *child) +{ + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (GTK_IS_WIDGET (child)); + + if (DZL_IS_DOCK_OVERLAY_EDGE (child)) + { + GtkPositionType edge; + + edge = dzl_dock_overlay_edge_get_edge (DZL_DOCK_OVERLAY_EDGE (child)); + + return !!(priv->child_revealed & (1 << edge)); + } + + return FALSE; +} + +typedef struct +{ + DzlDockOverlay *self; + GtkWidget *child; + GtkPositionType edge : 2; + guint revealing : 1; +} ChildRevealState; + +static void +child_reveal_state_free (gpointer data) +{ + ChildRevealState *state = (ChildRevealState *)data; + + g_object_unref (state->self); + g_object_unref (state->child); + + g_slice_free (ChildRevealState, state); +} + +static void +dzl_dock_overlay_child_reveal_done (gpointer user_data) +{ + ChildRevealState *state = (ChildRevealState *)user_data; + DzlDockOverlay *self = state->self; + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (GTK_IS_WIDGET (state->child)); + + if (state->revealing) + priv->child_revealed = priv->child_revealed | (1 << state->edge); + else + priv->child_revealed = priv->child_revealed & ~(1 << state->edge); + + gtk_container_child_notify_by_pspec (GTK_CONTAINER (self), + state->child, + child_properties [CHILD_PROP_REVEALED]); + + child_reveal_state_free (state); +} + +static void +dzl_dock_overlay_set_child_reveal (DzlDockOverlay *self, + GtkWidget *child, + gboolean reveal) +{ + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + ChildRevealState *state; + GtkPositionType edge; + guint child_reveal; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (GTK_IS_WIDGET (child)); + + if (!DZL_IS_DOCK_OVERLAY_EDGE (child)) + return; + + edge = dzl_dock_overlay_edge_get_edge (DZL_DOCK_OVERLAY_EDGE (child)); + + if (reveal) + child_reveal = priv->child_reveal | (1 << edge); + else + child_reveal = priv->child_reveal & ~(1 << edge); + + if (priv->child_reveal != child_reveal) + { + GtkAllocation alloc; + GdkMonitor *monitor; + GdkWindow *window; + guint duration = 0; + + state = g_slice_new0 (ChildRevealState); + state->self = g_object_ref (self); + state->child = g_object_ref (child); + state->edge = edge; + state->revealing = !!reveal; + + priv->child_reveal = child_reveal; + + window = gtk_widget_get_window (GTK_WIDGET (self)); + + if (window != NULL) + { + GdkDisplay *display = gtk_widget_get_display (child); + + monitor = gdk_display_get_monitor_at_window (display, window); + + gtk_widget_get_allocation (child, &alloc); + + if (edge == GTK_POS_LEFT || edge == GTK_POS_RIGHT) + duration = dzl_animation_calculate_duration (monitor, 0, alloc.width); + else + duration = dzl_animation_calculate_duration (monitor, 0, alloc.height); + } + +#if 0 + g_print ("Animating %s %d msec (currently at value=%lf)\n", + reveal ? "in" : "out", + duration, + gtk_adjustment_get_value (priv->edge_adj [edge])); +#endif + + dzl_object_animate_full (priv->edge_adj [edge], + DZL_ANIMATION_EASE_IN_OUT_CUBIC, + duration, + gtk_widget_get_frame_clock (child), + dzl_dock_overlay_child_reveal_done, + state, + "value", reveal ? 0.0 : 1.0, + NULL); + + gtk_container_child_notify_by_pspec (GTK_CONTAINER (self), + child, + child_properties [CHILD_PROP_REVEAL]); + } +} + +static gboolean +widget_descendant_contains_focus (GtkWidget *widget) +{ + GtkWidget *toplevel; + + g_assert (GTK_IS_WIDGET (widget)); + + toplevel = gtk_widget_get_toplevel (widget); + + if (GTK_IS_WINDOW (toplevel)) + { + GtkWidget *focus = gtk_window_get_focus (GTK_WINDOW (toplevel)); + + if (focus != NULL) + return gtk_widget_is_ancestor (focus, widget); + } + + return FALSE; +} + +static inline gboolean +rectangle_contains_point (const GdkRectangle *a, gint x, gint y) +{ + return x >= a->x && + x <= (a->x + a->width) && + y >= a->y && + y <= (a->y + a->height); +} + +static gboolean +dzl_dock_overlay_motion_notify_event (GtkWidget *widget, + GdkEventMotion *event) +{ + DzlDockOverlay *self = (DzlDockOverlay *)widget; + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + GdkWindow *iter; + GdkWindow *window; + gdouble x, y; + guint i; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (event != NULL); + + window = gtk_widget_get_window (widget); + + x = event->x; + y = event->y; + + for (iter = event->window; iter != window; iter = gdk_window_get_parent (iter)) + gdk_window_coords_to_parent (iter, x, y, &x, &y); + + for (i = 0; i < G_N_ELEMENTS (priv->hover_borders); i++) + { + DzlDockOverlayEdge *edge = priv->edges [i]; + GtkPositionType edge_type = dzl_dock_overlay_edge_get_position (edge); + GtkAllocation *hover_border = &priv->hover_borders [i]; + guint mask = 1 << edge_type; + + if (rectangle_contains_point (hover_border, x, y)) + { + /* Ignore this edge if it is already revealing */ + if (dzl_dock_overlay_get_child_reveal (self, GTK_WIDGET (edge)) || + dzl_dock_overlay_get_child_revealed (self, GTK_WIDGET (edge))) + continue; + + dzl_dock_overlay_set_child_reveal (self, GTK_WIDGET (edge), TRUE); + + priv->child_transient |= mask; + } + else if ((priv->child_transient & mask) != 0) + { + GtkWidget *event_widget = NULL; + GtkAllocation alloc; + gint rel_x; + gint rel_y; + + gdk_window_get_user_data (event->window, (gpointer *)&event_widget); + gtk_widget_get_allocation (GTK_WIDGET (edge), &alloc); + gtk_widget_translate_coordinates (event_widget, + GTK_WIDGET (edge), + event->x, + event->y, + &rel_x, + &rel_y); + + /* + * If this edge is transient, and the event window is not a + * descendant of the edges window, then we should dismiss the + * transient state. + */ + if (dzl_dock_overlay_get_child_revealed (self, GTK_WIDGET (edge)) && + !rectangle_contains_point (&alloc, rel_x, rel_y) && + !widget_descendant_contains_focus (GTK_WIDGET (edge))) + { + dzl_dock_overlay_set_child_reveal (self, GTK_WIDGET (edge), FALSE); + priv->child_transient &= ~mask; + } + } + } + + return GTK_WIDGET_CLASS (dzl_dock_overlay_parent_class)->motion_notify_event (widget, event); +} + +static void +dzl_dock_overlay_size_allocate (GtkWidget *widget, + GtkAllocation *alloc) +{ + DzlDockOverlay *self = (DzlDockOverlay *)widget; + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + GtkAllocation copy; + GtkAllocation *edge; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (alloc != NULL); + + copy = *alloc; + copy.x = 0; + copy.y = 0; + +#define GRAB_AREA 15 + + edge = &priv->hover_borders [GTK_POS_TOP]; + edge->x = copy.x; + edge->y = copy.y; + edge->width = copy.width; + edge->height = GRAB_AREA; + + edge = &priv->hover_borders [GTK_POS_LEFT]; + edge->x = copy.x; + edge->y = copy.y; + edge->width = GRAB_AREA; + edge->height = copy.height; + + edge = &priv->hover_borders [GTK_POS_RIGHT]; + edge->x = copy.x + copy.width - GRAB_AREA; + edge->y = copy.y; + edge->width = GRAB_AREA; + edge->height = copy.height; + + edge = &priv->hover_borders [GTK_POS_BOTTOM]; + edge->x = copy.x; + edge->y = copy.y + copy.height - GRAB_AREA; + edge->width = copy.width; + edge->height = GRAB_AREA; + +#undef GRAB_AREA + + GTK_WIDGET_CLASS (dzl_dock_overlay_parent_class)->size_allocate (widget, alloc); +} + +static void +dzl_dock_overlay_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlDockOverlay *self = DZL_DOCK_OVERLAY (object); + + switch (prop_id) + { + case PROP_MANAGER: + g_value_set_object (value, dzl_dock_item_get_manager (DZL_DOCK_ITEM (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_overlay_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlDockOverlay *self = DZL_DOCK_OVERLAY (object); + + switch (prop_id) + { + case PROP_MANAGER: + dzl_dock_item_set_manager (DZL_DOCK_ITEM (self), g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_overlay_get_child_property (GtkContainer *container, + GtkWidget *widget, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlDockOverlay *self = DZL_DOCK_OVERLAY (container); + + switch (prop_id) + { + case CHILD_PROP_REVEAL: + g_value_set_boolean (value, dzl_dock_overlay_get_child_reveal (self, widget)); + break; + + case CHILD_PROP_REVEALED: + g_value_set_boolean (value, dzl_dock_overlay_get_child_revealed (self, widget)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_dock_overlay_set_child_property (GtkContainer *container, + GtkWidget *widget, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlDockOverlay *self = DZL_DOCK_OVERLAY (container); + + switch (prop_id) + { + case CHILD_PROP_REVEAL: + dzl_dock_overlay_set_child_reveal (self, widget, g_value_get_boolean (value)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_dock_overlay_class_init (DzlDockOverlayClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + GtkBindingSet *binding_set; + + object_class->get_property = dzl_dock_overlay_get_property; + object_class->set_property = dzl_dock_overlay_set_property; + + widget_class->destroy = dzl_dock_overlay_destroy; + widget_class->hierarchy_changed = dzl_dock_overlay_hierarchy_changed; + widget_class->motion_notify_event = dzl_dock_overlay_motion_notify_event; + widget_class->size_allocate = dzl_dock_overlay_size_allocate; + + container_class->add = dzl_dock_overlay_add; + container_class->get_child_property = dzl_dock_overlay_get_child_property; + container_class->set_child_property = dzl_dock_overlay_set_child_property; + + klass->hide_edges = dzl_dock_overlay_hide_edges; + + g_object_class_override_property (object_class, PROP_MANAGER, "manager"); + + /* The difference between those two is: + * CHILD_PROP_REVEAL change its state at the animation start and can + * trigger a state change (so read/write capabilities) + * + * CHILD_PROP_REVEALED change its state at the animation end + * but is only readable. + */ + child_properties [CHILD_PROP_REVEAL] = + g_param_spec_boolean ("reveal", + "Reveal", + "If the panel edge should be revealed", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + child_properties [CHILD_PROP_REVEALED] = + g_param_spec_boolean ("revealed", + "Revealed", + "If the panel edge is revealed", + FALSE, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + gtk_container_class_install_child_properties (container_class, N_CHILD_PROPS, child_properties); + + gtk_widget_class_set_css_name (widget_class, "dzldockoverlay"); + + signals [HIDE_EDGES] = + g_signal_new ("hide-edges", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (DzlDockOverlayClass, hide_edges), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + binding_set = gtk_binding_set_by_class (klass); + + gtk_binding_entry_add_signal (binding_set, + GDK_KEY_Escape, + 0, + "hide-edges", + 0); +} + +static void +dzl_dock_overlay_init (DzlDockOverlay *self) +{ + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + guint i; + + gtk_widget_add_events (GTK_WIDGET (self), GDK_POINTER_MOTION_MASK); + + priv->overlay = g_object_new (GTK_TYPE_OVERLAY, + "visible", TRUE, + NULL); + + GTK_CONTAINER_CLASS (dzl_dock_overlay_parent_class)->add (GTK_CONTAINER (self), + GTK_WIDGET (priv->overlay)); + + g_signal_connect_object (priv->overlay, + "get-child-position", + G_CALLBACK (dzl_dock_overlay_get_child_position), + self, + G_CONNECT_SWAPPED); + + for (i = 0; i <= GTK_POS_BOTTOM; i++) + { + DzlDockOverlayEdge *edge; + + edge = g_object_new (DZL_TYPE_DOCK_OVERLAY_EDGE, + "edge", (GtkPositionType)i, + "visible", TRUE, + NULL); + + dzl_set_weak_pointer (&priv->edges[i], edge); + + gtk_overlay_add_overlay (priv->overlay, GTK_WIDGET (priv->edges [i])); + + priv->edge_adj [i] = gtk_adjustment_new (1, 0, 1, 0, 0, 0); + + g_signal_connect_object (priv->edge_adj [i], + "value-changed", + G_CALLBACK (gtk_widget_queue_allocate), + priv->overlay, + G_CONNECT_SWAPPED); + + priv->edge_handle_adj [i] = gtk_adjustment_new (0, 0, 1000, 0, 0, 0); + + g_signal_connect_object (priv->edge_handle_adj [i], + "value-changed", + G_CALLBACK (gtk_widget_queue_allocate), + priv->overlay, + G_CONNECT_SWAPPED); + } +} + +static void +dzl_dock_overlay_init_dock_iface (DzlDockInterface *iface) +{ +} + +GtkWidget * +dzl_dock_overlay_new (void) +{ + return g_object_new (DZL_TYPE_DOCK_OVERLAY, NULL); +} + +static void +dzl_dock_overlay_real_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + DzlDockOverlay *self = (DzlDockOverlay *)buildable; + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + DzlDockOverlayEdge *parent = NULL; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (builder == NULL || GTK_IS_BUILDER (builder)); + g_assert (G_IS_OBJECT (child)); + + if (!GTK_IS_WIDGET (child)) + { + g_warning ("Attempt to add a child of type \"%s\" to a \"%s\"", + G_OBJECT_TYPE_NAME (child), G_OBJECT_TYPE_NAME (self)); + return; + } + + if ((type == NULL) || (g_strcmp0 ("center", type) == 0)) + { + gtk_container_add (GTK_CONTAINER (priv->overlay), GTK_WIDGET (child)); + goto adopt; + } + + if (g_strcmp0 ("top", type) == 0) + parent = priv->edges [GTK_POS_TOP]; + else if (g_strcmp0 ("bottom", type) == 0) + parent = priv->edges [GTK_POS_BOTTOM]; + else if (g_strcmp0 ("right", type) == 0) + parent = priv->edges [GTK_POS_RIGHT]; + else + parent = priv->edges [GTK_POS_LEFT]; + + gtk_container_add (GTK_CONTAINER (parent), GTK_WIDGET (child)); + +adopt: + if (DZL_IS_DOCK_ITEM (child)) + dzl_dock_item_adopt (DZL_DOCK_ITEM (self), DZL_DOCK_ITEM (child)); +} + +static void +dzl_dock_overlay_init_buildable_iface (GtkBuildableIface *iface) +{ + iface->add_child = dzl_dock_overlay_real_add_child; +} + +/** + * dzl_dock_overlay_add_child: + * @self: a #DzlDockOverlay. + * @child: a #GtkWidget. + * @type: the type of the child to add (center, left, right, top, bottom). + * + */ +void +dzl_overlay_add_child (DzlDockOverlay *self, + GtkWidget *child, + const gchar *type) +{ + g_assert (DZL_IS_DOCK_OVERLAY (self)); + + dzl_dock_overlay_real_add_child (GTK_BUILDABLE (self), NULL, G_OBJECT (child), type); +} + +static void +dzl_dock_overlay_present_child (DzlDockItem *item, + DzlDockItem *child) +{ + DzlDockOverlay *self = (DzlDockOverlay *)item; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + g_assert (DZL_IS_DOCK_ITEM (child)); + + gtk_container_child_set (GTK_CONTAINER (self), GTK_WIDGET (child), + "reveal", TRUE, + NULL); +} + +static void +dzl_dock_overlay_update_visibility (DzlDockItem *item) +{ + DzlDockOverlay *self = (DzlDockOverlay *)item; + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + guint i; + + g_assert (DZL_IS_DOCK_OVERLAY (self)); + + for (i = 0; i < G_N_ELEMENTS (priv->edges); i++) + { + DzlDockOverlayEdge *edge = priv->edges [i]; + gboolean has_widgets; + + if (edge == NULL) + continue; + + has_widgets = dzl_dock_item_has_widgets (DZL_DOCK_ITEM (edge)); + + gtk_widget_set_child_visible (GTK_WIDGET (edge), has_widgets); + } + + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static void +dzl_dock_overlay_init_dock_item_iface (DzlDockItemInterface *iface) +{ + iface->present_child = dzl_dock_overlay_present_child; + iface->update_visibility = dzl_dock_overlay_update_visibility; +} + +/** + * dzl_dock_overlay_get_edge: + * @self: An #DzlDockOverlay. + * @position: the edge position. + * + * Returns: (transfer none): The corresponding #DzlDockOverlayEdge. + */ +DzlDockOverlayEdge * +dzl_dock_overlay_get_edge (DzlDockOverlay *self, + GtkPositionType position) +{ + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_OVERLAY (self), NULL); + + return priv->edges [position]; +} + +/** + * dzl_dock_overlay_get_edge_adjustment: + * @self: An #DzlDockOverlay. + * @position: the edge position. + * + * Returns: (transfer none): The corresponding #GtkAdjustment. + */ +GtkAdjustment * +dzl_dock_overlay_get_edge_adjustment (DzlDockOverlay *self, + GtkPositionType position) +{ + DzlDockOverlayPrivate *priv = dzl_dock_overlay_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_OVERLAY (self), NULL); + + return priv->edge_adj [position]; +} diff --git a/src/panel/dzl-dock-overlay.h b/src/panel/dzl-dock-overlay.h new file mode 100644 index 0000000..b26b081 --- /dev/null +++ b/src/panel/dzl-dock-overlay.h @@ -0,0 +1,63 @@ +/* dzl-dock-overlay.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_OVERLAY_H +#define DZL_DOCK_OVERLAY_H + +#include "dzl-version-macros.h" + +#include "dzl-dock.h" +#include "dzl-dock-overlay-edge.h" + +G_BEGIN_DECLS + +struct _DzlDockOverlayClass +{ + GtkEventBoxClass parent; + + void (*hide_edges) (DzlDockOverlay *self); + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_dock_overlay_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_overlay_add_child (DzlDockOverlay *self, + GtkWidget *child, + const gchar *type); +DZL_AVAILABLE_IN_ALL +DzlDockOverlayEdge *dzl_dock_overlay_get_edge (DzlDockOverlay *self, + GtkPositionType position); +DZL_AVAILABLE_IN_ALL +GtkAdjustment *dzl_dock_overlay_get_edge_adjustment (DzlDockOverlay *self, + GtkPositionType position); +G_END_DECLS + +#endif /* DZL_DOCK_OVERLAY_H */ diff --git a/src/panel/dzl-dock-paned-private.h b/src/panel/dzl-dock-paned-private.h new file mode 100644 index 0000000..fbb9985 --- /dev/null +++ b/src/panel/dzl-dock-paned-private.h @@ -0,0 +1,35 @@ +/* dzl-dock-paned-private.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_PANED_PRIVATE_H +#define DZL_DOCK_PANED_PRIVATE_H + +#include "dzl-dock-paned.h" + +G_BEGIN_DECLS + +void dzl_dock_paned_set_child_edge (DzlDockPaned *self, + GtkPositionType child_edge); + +G_END_DECLS + +#endif /* DZL_DOCK_PANED_PRIVATE_H */ diff --git a/src/panel/dzl-dock-paned.c b/src/panel/dzl-dock-paned.c new file mode 100644 index 0000000..5f57103 --- /dev/null +++ b/src/panel/dzl-dock-paned.c @@ -0,0 +1,108 @@ +/* dzl-dock-paned.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock-paned" + +#include "config.h" + +#include "dzl-dock-item.h" +#include "dzl-dock-paned.h" +#include "dzl-dock-paned-private.h" +#include "dzl-dock-stack.h" + +typedef struct +{ + GtkPositionType child_edge : 2; +} DzlDockPanedPrivate; + +G_DEFINE_TYPE_EXTENDED (DzlDockPaned, dzl_dock_paned, DZL_TYPE_MULTI_PANED, 0, + G_ADD_PRIVATE (DzlDockPaned) + G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK_ITEM, NULL)) + +static void +dzl_dock_paned_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlDockPaned *self = (DzlDockPaned *)container; + DzlDockPanedPrivate *priv = dzl_dock_paned_get_instance_private (self); + + g_assert (DZL_IS_DOCK_PANED (self)); + + if (DZL_IS_DOCK_STACK (widget)) + dzl_dock_stack_set_edge (DZL_DOCK_STACK (widget), priv->child_edge); + + GTK_CONTAINER_CLASS (dzl_dock_paned_parent_class)->add (container, widget); + + if (DZL_IS_DOCK_ITEM (widget)) + dzl_dock_item_adopt (DZL_DOCK_ITEM (self), DZL_DOCK_ITEM (widget)); +} + +static void +dzl_dock_paned_class_init (DzlDockPanedClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + container_class->add = dzl_dock_paned_add; + + gtk_widget_class_set_css_name (widget_class, "dzldockpaned"); +} + +static void +dzl_dock_paned_init (DzlDockPaned *self) +{ + DzlDockPanedPrivate *priv = dzl_dock_paned_get_instance_private (self); + + priv->child_edge = GTK_POS_TOP; +} + +GtkWidget * +dzl_dock_paned_new (void) +{ + return g_object_new (DZL_TYPE_DOCK_PANED, NULL); +} + +static void +dzl_dock_paned_update_child_edge (GtkWidget *widget, + gpointer user_data) +{ + GtkPositionType child_edge = GPOINTER_TO_INT (user_data); + + g_assert (GTK_IS_WIDGET (widget)); + + if (DZL_IS_DOCK_STACK (widget)) + dzl_dock_stack_set_edge (DZL_DOCK_STACK (widget), child_edge); +} + +void +dzl_dock_paned_set_child_edge (DzlDockPaned *self, + GtkPositionType child_edge) +{ + DzlDockPanedPrivate *priv = dzl_dock_paned_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_PANED (self)); + + if (priv->child_edge != child_edge) + { + priv->child_edge = child_edge; + + gtk_container_foreach (GTK_CONTAINER (self), + dzl_dock_paned_update_child_edge, + GINT_TO_POINTER (child_edge)); + } +} diff --git a/src/panel/dzl-dock-paned.h b/src/panel/dzl-dock-paned.h new file mode 100644 index 0000000..e0cb911 --- /dev/null +++ b/src/panel/dzl-dock-paned.h @@ -0,0 +1,51 @@ +/* dzl-dock-paned.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_PANED_H +#define DZL_DOCK_PANED_H + +#include "dzl-version-macros.h" + +#include "dzl-dock-types.h" + +G_BEGIN_DECLS + +struct _DzlDockPanedClass +{ + DzlMultiPanedClass parent; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_dock_paned_new (void); + +G_END_DECLS + +#endif /* DZL_DOCK_PANED_H */ diff --git a/src/panel/dzl-dock-revealer.c b/src/panel/dzl-dock-revealer.c new file mode 100644 index 0000000..eb4d102 --- /dev/null +++ b/src/panel/dzl-dock-revealer.c @@ -0,0 +1,1044 @@ +/* dzl-dock-revealer.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock-revealer" + +#include "config.h" + +#include "animation/dzl-animation.h" +#include "panel/dzl-dock-revealer.h" +#include "util/dzl-util-private.h" + +/** + * SECTION:dzldockrevealer + * @title: DzlDockRevealer + * @short_description: A panel revealer + * + * This widget is a bit like #GtkRevealer with a couple of important + * differences. First, it only supports a couple transition types + * (the direction to slide reveal). Additionally, the size of the + * child allocation will not change during the animation. This is not + * as generally useful as an upstream GTK+ widget, but is extremely + * important for the panel case to avoid things looking strange while + * animating into and out of view. + */ + +#define IS_HORIZONTAL(type) \ + (((type) == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT) || \ + ((type) == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT)) + +#define IS_VERTICAL(type) \ + (((type) == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_UP) || \ + ((type) == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN)) + +typedef struct +{ + DzlAnimation *animation; + GtkAdjustment *adjustment; + GdkWindow *window; + gint position; + gint position_tmp; + guint transition_duration; + DzlDockRevealerTransitionType transition_type : 3; + guint position_set : 1; + guint reveal_child : 1; + guint child_revealed : 1; + GtkRequisition nat_req; +} DzlDockRevealerPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlDockRevealer, dzl_dock_revealer, DZL_TYPE_BIN) + +enum { + PROP_0, + PROP_CHILD_REVEALED, + PROP_POSITION, + PROP_POSITION_SET, + PROP_REVEAL_CHILD, + PROP_TRANSITION_DURATION, + PROP_TRANSITION_TYPE, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +GtkWidget * +dzl_dock_revealer_new (void) +{ + return g_object_new (DZL_TYPE_DOCK_REVEALER, NULL); +} + +guint +dzl_dock_revealer_get_transition_duration (DzlDockRevealer *self) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), 0); + + return priv->transition_duration; +} + +void +dzl_dock_revealer_set_transition_duration (DzlDockRevealer *self, + guint transition_duration) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_REVEALER (self)); + + if (priv->transition_duration != transition_duration) + { + priv->transition_duration = transition_duration; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TRANSITION_DURATION]); + } +} + +DzlDockRevealerTransitionType +dzl_dock_revealer_get_transition_type (DzlDockRevealer *self) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), 0); + + return priv->transition_type; +} + +void +dzl_dock_revealer_set_transition_type (DzlDockRevealer *self, + DzlDockRevealerTransitionType transition_type) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_REVEALER (self)); + g_return_if_fail (transition_type >= DZL_DOCK_REVEALER_TRANSITION_TYPE_NONE); + g_return_if_fail (transition_type <= DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); + + if (priv->transition_type != transition_type) + { + priv->transition_type = transition_type; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TRANSITION_TYPE]); + } +} + +gboolean +dzl_dock_revealer_get_child_revealed (DzlDockRevealer *self) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), FALSE); + + return priv->child_revealed; +} + +gboolean +dzl_dock_revealer_get_reveal_child (DzlDockRevealer *self) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), FALSE); + + return priv->reveal_child; +} + +static void +dzl_dock_revealer_animation_done (gpointer user_data) +{ + g_autoptr(DzlDockRevealer) self = user_data; + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + GtkWidget *child; + gboolean child_revealed = FALSE; + gboolean child_visible = FALSE; + + g_assert (DZL_DOCK_REVEALER (self)); + + child = gtk_bin_get_child (GTK_BIN (self)); + + if (priv->adjustment != NULL) + { + child_revealed = gtk_adjustment_get_value (priv->adjustment) >= 1.0; + child_visible = gtk_adjustment_get_value (priv->adjustment) != 0.0; + } + + if (child != NULL) + gtk_widget_set_child_visible (GTK_WIDGET (child), child_visible); + + priv->child_revealed = child_revealed; + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHILD_REVEALED]); + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static guint +size_to_duration (GdkMonitor *monitor, + gint size) +{ + g_assert (!monitor || GDK_IS_MONITOR (monitor)); + + if (monitor != NULL) + return dzl_animation_calculate_duration (monitor, 0, size); + + return MAX (150, size * 1.2); +} + +static guint +dzl_dock_revealer_calculate_duration (DzlDockRevealer *self) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + GtkWidget *child; + GdkWindow *window; + GdkMonitor *monitor = NULL; + GdkDisplay *display; + GtkRequisition min_size; + GtkRequisition nat_size; + + g_assert (DZL_IS_DOCK_REVEALER (self)); + + if (!gtk_widget_get_realized (GTK_WIDGET (self))) + return 0; + + child = gtk_bin_get_child (GTK_BIN (self)); + + if (child == NULL) + return 0; + + if (priv->transition_type == DZL_DOCK_REVEALER_TRANSITION_TYPE_NONE) + return 0; + + if (priv->transition_duration != 0) + return priv->transition_duration; + + gtk_widget_get_preferred_size (child, &min_size, &nat_size); + + display = gtk_widget_get_display (GTK_WIDGET (self)); + window = gtk_widget_get_window (GTK_WIDGET (self)); + if (window != NULL) + monitor = gdk_display_get_monitor_at_window (display, window); + + if (IS_HORIZONTAL (priv->transition_type)) + { + if (priv->position_set) + { + if (priv->position_set && priv->position > min_size.width) + return size_to_duration (monitor, priv->position); + return size_to_duration (monitor, min_size.width); + } + + return size_to_duration (monitor, nat_size.width); + } + else + { + if (priv->position_set) + { + if (priv->position_set && priv->position > min_size.height) + return size_to_duration (monitor, priv->position); + return size_to_duration (monitor, min_size.height); + } + + return size_to_duration (monitor, nat_size.height); + } +} + +void +dzl_dock_revealer_set_reveal_child (DzlDockRevealer *self, + gboolean reveal_child) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_REVEALER (self)); + + reveal_child = !!reveal_child; + + if (reveal_child != priv->reveal_child) + { + GtkWidget *child = gtk_bin_get_child (GTK_BIN (self)); + + priv->reveal_child = reveal_child; + + dzl_animation_stop (priv->animation); + dzl_clear_weak_pointer (&priv->animation); + + if (child != NULL) + { + guint duration; + + gtk_widget_set_child_visible (child, TRUE); + + duration = dzl_dock_revealer_calculate_duration (self); + + if (duration == 0) + { + gtk_adjustment_set_value (priv->adjustment, reveal_child ? 1.0 : 0.0); + priv->child_revealed = reveal_child; + gtk_widget_set_child_visible (child, reveal_child); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_REVEAL_CHILD]); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHILD_REVEALED]); + } + else + { + DzlAnimation *animation; + + animation = dzl_object_animate_full (priv->adjustment, + DZL_ANIMATION_EASE_IN_OUT_CUBIC, + duration, + gtk_widget_get_frame_clock (GTK_WIDGET (self)), + dzl_dock_revealer_animation_done, + g_object_ref (self), + "value", reveal_child ? 1.0 : 0.0, + NULL); + dzl_set_weak_pointer (&priv->animation, animation); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_REVEAL_CHILD]); + } + + gtk_widget_queue_resize (GTK_WIDGET (self)); + } + } +} + +gint +dzl_dock_revealer_get_position (DzlDockRevealer *self) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), 0); + + return priv->position; +} + +void +dzl_dock_revealer_set_position (DzlDockRevealer *self, + gint position) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_REVEALER (self)); + g_return_if_fail (position >= 0); + + if (priv->position != position) + { + priv->position = position; + + if (!priv->position_set) + { + priv->position_set = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSITION_SET]); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSITION]); + gtk_widget_queue_resize (GTK_WIDGET (self)); + } +} + +gboolean +dzl_dock_revealer_get_position_set (DzlDockRevealer *self) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), FALSE); + + return priv->position_set; +} + +void +dzl_dock_revealer_set_position_set (DzlDockRevealer *self, + gboolean position_set) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_REVEALER (self)); + + position_set = !!position_set; + + if (priv->position_set != position_set) + { + priv->position_set = position_set; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSITION_SET]); + gtk_widget_queue_resize (GTK_WIDGET (self)); + } +} + +static void +dzl_dock_revealer_get_child_preferred_width (DzlDockRevealer *self, + gint *min_width, + gint *nat_width) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + GtkWidget *child; + + g_assert (DZL_IS_DOCK_REVEALER (self)); + g_assert (min_width != NULL); + g_assert (nat_width != NULL); + + *min_width = 0; + *nat_width = 0; + + if (NULL == (child = gtk_bin_get_child (GTK_BIN (self)))) + return; + + if (!gtk_widget_get_child_visible (child) || !gtk_widget_get_visible (child)) + return; + + gtk_widget_get_preferred_width (child, min_width, nat_width); + + if (IS_HORIZONTAL (priv->transition_type) && priv->position_set) + { + if (priv->position > *min_width) + *nat_width = priv->position; + else + *nat_width = *min_width; + } +} + +static void +dzl_dock_revealer_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + DzlDockRevealer *self = (DzlDockRevealer *)widget; + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + GtkStyleContext *style_context; + GtkWidget *child; + GtkBorder borders; + + g_assert (DZL_IS_DOCK_REVEALER (self)); + g_assert (min_width != NULL); + g_assert (nat_width != NULL); + + style_context = gtk_widget_get_style_context (widget); + dzl_gtk_style_context_get_borders (style_context, &borders); + + child = gtk_bin_get_child (GTK_BIN (self)); + + dzl_dock_revealer_get_child_preferred_width (self, min_width, nat_width); + + *min_width += borders.left + borders.right; + *nat_width += borders.left + borders.right; + + priv->nat_req.width = *nat_width; + + if (IS_HORIZONTAL (priv->transition_type)) + { + if (priv->animation != NULL) + { + /* + * We allow going smaller than the minimum size during animations + * and rely on clipping to hide the child. + */ + *min_width = 0; + + /* + * Our natural width is adjusted for the in-progress animation. + */ + *nat_width *= gtk_adjustment_get_value (priv->adjustment); + } + else if (child != NULL && !gtk_widget_get_child_visible (child)) + { + /* + * Make sure we are completely hidden if the child is not currently + * visible. + */ + *min_width = 0; + *nat_width = 0; + } + } +} + +static void +dzl_dock_revealer_get_child_preferred_height (DzlDockRevealer *self, + gint *min_height, + gint *nat_height) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + GtkWidget *child; + + g_assert (DZL_IS_DOCK_REVEALER (self)); + g_assert (min_height != NULL); + g_assert (nat_height != NULL); + + *min_height = 0; + *nat_height = 0; + + if (NULL == (child = gtk_bin_get_child (GTK_BIN (self)))) + return; + + if (!gtk_widget_get_child_visible (child) || !gtk_widget_get_visible (child)) + return; + + gtk_widget_get_preferred_height (child, min_height, nat_height); + + if (IS_VERTICAL (priv->transition_type) && priv->position_set) + { + if (priv->position > *min_height) + *nat_height = priv->position; + else + *nat_height = *min_height; + } +} + +static void +dzl_dock_revealer_get_preferred_height (GtkWidget *widget, + gint *min_height, + gint *nat_height) +{ + DzlDockRevealer *self = (DzlDockRevealer *)widget; + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + GtkStyleContext *style_context; + GtkWidget *child; + GtkBorder borders; + + g_assert (DZL_IS_DOCK_REVEALER (self)); + g_assert (min_height != NULL); + g_assert (nat_height != NULL); + + style_context = gtk_widget_get_style_context (widget); + dzl_gtk_style_context_get_borders (style_context, &borders); + + child = gtk_bin_get_child (GTK_BIN (self)); + + dzl_dock_revealer_get_child_preferred_height (self, min_height, nat_height); + + *min_height += borders.top + borders.bottom; + *nat_height += borders.top + borders.bottom; + + priv->nat_req.height = *nat_height; + + if (IS_VERTICAL (priv->transition_type)) + { + if (priv->animation != NULL) + { + /* + * We allow going smaller than the minimum size during animations + * and rely on clipping to hide the child. + */ + *min_height = 0; + + /* + * Our natural height is adjusted for the in-progress animation. + */ + *nat_height *= gtk_adjustment_get_value (priv->adjustment); + } + else if (child != NULL && !gtk_widget_get_child_visible (child)) + { + /* + * Make sure we are completely hidden if the child is not currently + * visible. + */ + *min_height = 0; + *nat_height = 0; + } + } +} + +static void +dzl_dock_revealer_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + DzlDockRevealer *self = (DzlDockRevealer *)widget; + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + GtkAllocation child_allocation; + GtkRequisition min_req; + GtkRequisition nat_req; + GtkStyleContext *style_context; + GtkWidget *child; + GtkBorder borders; + + g_assert (DZL_IS_DOCK_REVEALER (self)); + + gtk_widget_set_allocation (widget, allocation); + + if (gtk_widget_get_realized (GTK_WIDGET (self))) + gdk_window_move_resize (priv->window, + allocation->x, + allocation->y, + allocation->width, + allocation->height); + + if (NULL == (child = gtk_bin_get_child (GTK_BIN (self)))) + return; + + if (!gtk_widget_get_child_visible (child)) + return; + + child_allocation = *allocation; + child_allocation.x = 0; + child_allocation.y = 0; + + style_context = gtk_widget_get_style_context (widget); + dzl_gtk_style_context_get_borders (style_context, &borders); + dzl_gtk_allocation_subtract_border (&child_allocation, &borders); + + if (IS_HORIZONTAL (priv->transition_type)) + { + dzl_dock_revealer_get_child_preferred_width (self, &min_req.width, &nat_req.width); + child_allocation.width = nat_req.width; + + if (priv->transition_type == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT) + child_allocation.x = allocation->width - borders.right - child_allocation.width; + } + else if (IS_VERTICAL (priv->transition_type)) + { + dzl_dock_revealer_get_child_preferred_height (self, &min_req.height, &nat_req.height); + child_allocation.height = nat_req.height; + + if (priv->transition_type == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN) + child_allocation.y = allocation->height - borders.bottom - child_allocation.height; + } + + gtk_widget_size_allocate (child, &child_allocation); +} + +static void +dzl_dock_revealer_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlDockRevealer *self = (DzlDockRevealer *)container; + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_assert (DZL_IS_DOCK_REVEALER (self)); + g_assert (GTK_IS_WIDGET (widget)); + + GTK_CONTAINER_CLASS (dzl_dock_revealer_parent_class)->add (container, widget); + + gtk_widget_set_child_visible (widget, priv->reveal_child); + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static gboolean +dzl_dock_revealer_draw (GtkWidget *widget, + cairo_t *cr) +{ + DzlDockRevealer *self = (DzlDockRevealer *)widget; + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + GtkAllocation alloc; + GtkStyleContext *style_context; + GtkWidget *child; + GtkBorder margin; + GtkStateFlags state; + + g_assert (DZL_IS_DOCK_REVEALER (self)); + + gtk_widget_get_allocation (widget, &alloc); + + if (gtk_widget_get_has_window (widget)) + { + alloc.x = 0; + alloc.y = 0; + } + + if (priv->animation != NULL) + { + /* + * If we are currently animating, we want to ensure that our background + * is drawn at the size of the natural allocation. Otherwise borders + * will be shown in improper locations. + */ + if (IS_HORIZONTAL (priv->transition_type)) + { + if (priv->transition_type == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT) + alloc.x -= (priv->nat_req.width - alloc.width); + alloc.width = priv->nat_req.width; + } + else + { + if (priv->transition_type == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN) + alloc.y -= (priv->nat_req.height - alloc.height); + alloc.height = priv->nat_req.height; + } + } + + style_context = gtk_widget_get_style_context (widget); + state = gtk_style_context_get_state (style_context); + + gtk_style_context_get_margin (style_context, state, &margin); + dzl_gtk_allocation_subtract_border (&alloc, &margin); + + gtk_render_background (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height); + + child = gtk_bin_get_child (GTK_BIN (widget)); + if (child != NULL) + gtk_container_propagate_draw (GTK_CONTAINER (widget), child, cr); + + gtk_render_frame (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height); + + return FALSE; +} + +static void +dzl_dock_revealer_realize (GtkWidget *widget) +{ + DzlDockRevealer *self = (DzlDockRevealer *)widget; + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + GdkWindowAttr attributes = { 0 }; + GdkWindow *parent; + GtkAllocation alloc; + gint attributes_mask = 0; + + g_assert (DZL_IS_DOCK_REVEALER (widget)); + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + gtk_widget_set_realized (GTK_WIDGET (self), TRUE); + + parent = gtk_widget_get_parent_window (GTK_WIDGET (self)); + + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gtk_widget_get_visual (GTK_WIDGET (self)); + attributes.x = alloc.x; + attributes.y = alloc.y; + attributes.width = alloc.width; + attributes.height = alloc.height; + attributes.event_mask = 0; + + attributes_mask = (GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL); + + priv->window = gdk_window_new (parent, &attributes, attributes_mask); + gtk_widget_set_window (GTK_WIDGET (self), priv->window); + gtk_widget_register_window (GTK_WIDGET (self), priv->window); +} + +static void +dzl_dock_revealer_destroy (GtkWidget *widget) +{ + DzlDockRevealer *self = (DzlDockRevealer *)widget; + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_clear_object (&priv->adjustment); + dzl_clear_weak_pointer (&priv->animation); + + GTK_WIDGET_CLASS (dzl_dock_revealer_parent_class)->destroy (widget); +} + +static void +dzl_dock_revealer_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlDockRevealer *self = DZL_DOCK_REVEALER (object); + + switch (prop_id) + { + case PROP_CHILD_REVEALED: + g_value_set_boolean (value, dzl_dock_revealer_get_child_revealed (self)); + break; + + case PROP_POSITION: + g_value_set_int (value, dzl_dock_revealer_get_position (self)); + break; + + case PROP_POSITION_SET: + g_value_set_boolean (value, dzl_dock_revealer_get_position_set (self)); + break; + + case PROP_REVEAL_CHILD: + g_value_set_boolean (value, dzl_dock_revealer_get_reveal_child (self)); + break; + + case PROP_TRANSITION_DURATION: + g_value_set_uint (value, dzl_dock_revealer_get_transition_duration (self)); + break; + + case PROP_TRANSITION_TYPE: + g_value_set_enum (value, dzl_dock_revealer_get_transition_type (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_revealer_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlDockRevealer *self = DZL_DOCK_REVEALER (object); + + switch (prop_id) + { + case PROP_REVEAL_CHILD: + dzl_dock_revealer_set_reveal_child (self, g_value_get_boolean (value)); + break; + + case PROP_POSITION: + dzl_dock_revealer_set_position (self, g_value_get_int (value)); + break; + + case PROP_POSITION_SET: + dzl_dock_revealer_set_position_set (self, g_value_get_boolean (value)); + break; + + case PROP_TRANSITION_DURATION: + dzl_dock_revealer_set_transition_duration (self, g_value_get_uint (value)); + break; + + case PROP_TRANSITION_TYPE: + dzl_dock_revealer_set_transition_type (self, g_value_get_enum (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_revealer_class_init (DzlDockRevealerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->get_property = dzl_dock_revealer_get_property; + object_class->set_property = dzl_dock_revealer_set_property; + + widget_class->destroy = dzl_dock_revealer_destroy; + widget_class->get_preferred_width = dzl_dock_revealer_get_preferred_width; + widget_class->get_preferred_height = dzl_dock_revealer_get_preferred_height; + widget_class->realize = dzl_dock_revealer_realize; + widget_class->size_allocate = dzl_dock_revealer_size_allocate; + widget_class->draw = dzl_dock_revealer_draw; + + container_class->add = dzl_dock_revealer_add; + + properties [PROP_CHILD_REVEALED] = + g_param_spec_boolean ("child-revealed", + "Child Revealed", + "If the child is fully revealed", + TRUE, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_POSITION] = + g_param_spec_int ("position", + "Position", + "Position", + 0, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_POSITION_SET] = + g_param_spec_boolean ("position-set", + "Position Set", + "If the position has been set", + FALSE, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_REVEAL_CHILD] = + g_param_spec_boolean ("reveal-child", + "Reveal Child", + "If the child should be revealed", + FALSE, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TRANSITION_DURATION] = + g_param_spec_uint ("transition-duration", + "Transition Duration", + "Length of duration in milliseconds", + 0, + G_MAXUINT, + 0, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TRANSITION_TYPE] = + g_param_spec_enum ("transition-type", + "Transition Type", + "Transition Type", + DZL_TYPE_DOCK_REVEALER_TRANSITION_TYPE, + DZL_DOCK_REVEALER_TRANSITION_TYPE_NONE, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_dock_revealer_init (DzlDockRevealer *self) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + gtk_widget_set_has_window (GTK_WIDGET (self), TRUE); + + priv->reveal_child = FALSE; + priv->child_revealed = FALSE; + + priv->transition_duration = 0; + + priv->adjustment = g_object_new (GTK_TYPE_ADJUSTMENT, + "lower", 0.0, + "upper", 1.0, + "value", 0.0, + NULL); + + g_signal_connect_object (priv->adjustment, + "value-changed", + G_CALLBACK (gtk_widget_queue_resize), + self, + G_CONNECT_SWAPPED); +} + +GType +dzl_dock_revealer_transition_type_get_type (void) +{ + static GType type_id; + + if (g_once_init_enter (&type_id)) + { + GType _type_id; + static const GEnumValue values[] = { + { DZL_DOCK_REVEALER_TRANSITION_TYPE_NONE, + "DZL_DOCK_REVEALER_TRANSITION_TYPE_NONE", + "none" }, + { DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT, + "DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT", + "slide-right" }, + { DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT, + "DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT", + "slide-left" }, + { DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_UP, + "DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_UP", + "slide-up" }, + { DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN, + "DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN", + "slide-down" }, + { 0 } + }; + + _type_id = g_enum_register_static ("DzlDockRevealerTransitionType", values); + + g_once_init_leave (&type_id, _type_id); + } + + return type_id; +} + +static void +dzl_dock_revealer_animate_to_position_done (gpointer user_data) +{ + g_autoptr(DzlDockRevealer) self = user_data; + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_assert (DZL_DOCK_REVEALER (self)); + + if (priv->adjustment != NULL) + { + gboolean child_revealed; + + child_revealed = (priv->position_tmp > 0); + if (priv->child_revealed != child_revealed) + { + GtkWidget *child = gtk_bin_get_child (GTK_BIN (self)); + + priv->child_revealed = child_revealed; + gtk_widget_set_child_visible (GTK_WIDGET (child), child_revealed); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHILD_REVEALED]); + + gtk_adjustment_set_value (priv->adjustment, child_revealed ? 1.0 : 0.0); + priv->position = priv->position_tmp; + gtk_widget_queue_resize (GTK_WIDGET (self)); + } +} + +void +dzl_dock_revealer_animate_to_position (DzlDockRevealer *self, + gint position, + guint transition_duration) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + gdouble current_position; + gdouble value; + + g_return_if_fail (DZL_IS_DOCK_REVEALER (self)); + + if (transition_duration == 0) + transition_duration = dzl_dock_revealer_calculate_duration (self); + + current_position = priv->position; + if (current_position != position) + { + DzlAnimation *animation; + GtkWidget *child; + + priv->reveal_child = (position > 0); + priv->position_tmp = position; + if (!priv->position_set) + { + priv->position_set = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSITION_SET]); + } + + if (current_position < position) + { + value = 1.0; + if (current_position > 0) + { + priv->position = position; + gtk_adjustment_set_value (priv->adjustment, current_position / position); + } + } + else + value = position / current_position; + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSITION]); + + child = gtk_bin_get_child (GTK_BIN (self)); + if (child != NULL) + { + if (priv->animation != NULL) + { + dzl_animation_stop (priv->animation); + dzl_clear_weak_pointer (&priv->animation); + } + + gtk_widget_set_child_visible (child, TRUE); + animation = dzl_object_animate_full (priv->adjustment, + DZL_ANIMATION_EASE_IN_OUT_CUBIC, + transition_duration, + gtk_widget_get_frame_clock (GTK_WIDGET (self)), + dzl_dock_revealer_animate_to_position_done, + g_object_ref (self), + "value", value, + NULL); + + dzl_set_weak_pointer (&priv->animation, animation); + } + + if ((priv->reveal_child && position == 0) || (!priv->reveal_child && position != 0)) + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_REVEAL_CHILD]); + } +} + +/** + * dzl_dock_revealer_is_animating: + * @self: a #DzlDockRevealer + * + * This is a helper to check if the revealer is animating. You probably don't + * want to poll this function. Connect to notify::child-revealed or + * notify::reveal-child instead. + * + * Returns: %TRUE if an animation is in progress. + */ +gboolean +dzl_dock_revealer_is_animating (DzlDockRevealer *self) +{ + DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), FALSE); + + return (priv->animation != NULL); +} diff --git a/src/panel/dzl-dock-revealer.h b/src/panel/dzl-dock-revealer.h new file mode 100644 index 0000000..f793431 --- /dev/null +++ b/src/panel/dzl-dock-revealer.h @@ -0,0 +1,101 @@ +/* dzl-dock-revealer.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_REVEALER_H +#define DZL_DOCK_REVEALER_H + +#include "dzl-version-macros.h" + +#include "widgets/dzl-bin.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_DOCK_REVEALER_TRANSITION_TYPE (dzl_dock_revealer_transition_type_get_type()) +#define DZL_TYPE_DOCK_REVEALER (dzl_dock_revealer_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlDockRevealer, dzl_dock_revealer, DZL, DOCK_REVEALER, DzlBin) + +typedef enum +{ + DZL_DOCK_REVEALER_TRANSITION_TYPE_NONE, + DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT, + DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT, + DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_UP, + DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN, +} DzlDockRevealerTransitionType; + +struct _DzlDockRevealerClass +{ + DzlBinClass parent; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +GType dzl_dock_revealer_transition_type_get_type (void); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_dock_revealer_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_dock_revealer_animate_to_position (DzlDockRevealer *self, + gint position, + guint transition_duration); +DZL_AVAILABLE_IN_ALL +DzlDockRevealerTransitionType dzl_dock_revealer_get_transition_type (DzlDockRevealer *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_revealer_set_transition_type (DzlDockRevealer *self, + DzlDockRevealerTransitionType transition_type); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_revealer_get_child_revealed (DzlDockRevealer *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_revealer_set_reveal_child (DzlDockRevealer *self, + gboolean reveal_child); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_revealer_get_reveal_child (DzlDockRevealer *self); +DZL_AVAILABLE_IN_ALL +gint dzl_dock_revealer_get_position (DzlDockRevealer *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_revealer_set_position (DzlDockRevealer *self, + gint position); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_revealer_get_position_set (DzlDockRevealer *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_revealer_set_position_set (DzlDockRevealer *self, + gboolean position_set); +DZL_AVAILABLE_IN_ALL +guint dzl_dock_revealer_get_transition_duration (DzlDockRevealer *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_revealer_set_transition_duration (DzlDockRevealer *self, + guint transition_duration); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_revealer_is_animating (DzlDockRevealer *self); + +G_END_DECLS + +#endif /* DZL_DOCK_REVEALER_H */ diff --git a/src/panel/dzl-dock-stack.c b/src/panel/dzl-dock-stack.c new file mode 100644 index 0000000..5086a8a --- /dev/null +++ b/src/panel/dzl-dock-stack.c @@ -0,0 +1,480 @@ +/* dzl-dock-stack.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock-stack" + +#include "config.h" + +#include "panel/dzl-dock-item.h" +#include "panel/dzl-dock-stack.h" +#include "panel/dzl-dock-widget.h" +#include "panel/dzl-tab-private.h" +#include "panel/dzl-tab-strip.h" +#include "util/dzl-util-private.h" + +typedef struct +{ + GtkStack *stack; + DzlTabStrip *tab_strip; + GtkButton *pinned_button; + GtkPositionType edge : 2; + DzlTabStyle style : 2; +} DzlDockStackPrivate; + +static void dzl_dock_stack_init_dock_item_iface (DzlDockItemInterface *iface); + +G_DEFINE_TYPE_EXTENDED (DzlDockStack, dzl_dock_stack, GTK_TYPE_BOX, 0, + G_ADD_PRIVATE (DzlDockStack) + G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK_ITEM, + dzl_dock_stack_init_dock_item_iface)) + +enum { + PROP_0, + PROP_EDGE, + PROP_SHOW_PINNED_BUTTON, + PROP_STYLE, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_dock_stack_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlDockStack *self = (DzlDockStack *)container; + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + g_autofree gchar *icon_name = NULL; + g_autofree gchar *title = NULL; + + g_assert (DZL_IS_DOCK_STACK (self)); + + if (DZL_IS_DOCK_ITEM (widget)) + { + title = dzl_dock_item_get_title (DZL_DOCK_ITEM (widget)); + icon_name = dzl_dock_item_get_icon_name (DZL_DOCK_ITEM (widget)); + } + + gtk_container_add_with_properties (GTK_CONTAINER (priv->stack), widget, + "icon-name", icon_name, + "title", title, + NULL); + + if (DZL_IS_DOCK_ITEM (widget)) + dzl_dock_item_adopt (DZL_DOCK_ITEM (self), DZL_DOCK_ITEM (widget)); +} + +static void +dzl_dock_stack_grab_focus (GtkWidget *widget) +{ + DzlDockStack *self = (DzlDockStack *)widget; + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + GtkWidget *child; + + g_assert (DZL_IS_DOCK_STACK (self)); + + child = gtk_stack_get_visible_child (priv->stack); + + if (child != NULL) + gtk_widget_grab_focus (GTK_WIDGET (priv->stack)); + else + GTK_WIDGET_CLASS (dzl_dock_stack_parent_class)->grab_focus (widget); +} + +static void +dzl_dock_stack_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlDockStack *self = DZL_DOCK_STACK (object); + + switch (prop_id) + { + case PROP_EDGE: + g_value_set_enum (value, dzl_dock_stack_get_edge (self)); + break; + + case PROP_SHOW_PINNED_BUTTON: + g_value_set_boolean (value, dzl_dock_stack_get_show_pinned_button (self)); + break; + + case PROP_STYLE: + g_value_set_flags (value, dzl_dock_stack_get_style (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_stack_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlDockStack *self = DZL_DOCK_STACK (object); + + switch (prop_id) + { + case PROP_EDGE: + dzl_dock_stack_set_edge (self, g_value_get_enum (value)); + break; + + case PROP_SHOW_PINNED_BUTTON: + dzl_dock_stack_set_show_pinned_button (self, g_value_get_boolean (value)); + break; + + case PROP_STYLE: + dzl_dock_stack_set_style (self, g_value_get_flags (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_stack_class_init (DzlDockStackClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->get_property = dzl_dock_stack_get_property; + object_class->set_property = dzl_dock_stack_set_property; + + widget_class->grab_focus = dzl_dock_stack_grab_focus; + + container_class->add = dzl_dock_stack_add; + + properties [PROP_EDGE] = + g_param_spec_enum ("edge", + "Edge", + "The edge for the tab strip", + GTK_TYPE_POSITION_TYPE, + GTK_POS_TOP, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_PINNED_BUTTON] = + g_param_spec_boolean ("show-pinned-button", + "Show Pinned Button", + "Show the pinned button to pin the dock edge", + FALSE, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_STYLE] = + g_param_spec_flags ("style", + "Style", + "Style", + DZL_TYPE_TAB_STYLE, + DZL_TAB_BOTH, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_css_name (widget_class, "dzldockstack"); +} + +static void +dzl_dock_stack_init (DzlDockStack *self) +{ + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + + gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_VERTICAL); + + priv->style = DZL_TAB_BOTH; + priv->edge = GTK_POS_TOP; + + /* + * NOTE: setting a transition for the stack seems to muck up + * focus, causing the old-tab to get refocused. So we can't + * switch to CROSSFADE just yet. + */ + + priv->stack = g_object_new (GTK_TYPE_STACK, + "homogeneous", FALSE, + "visible", TRUE, + NULL); + + priv->tab_strip = g_object_new (DZL_TYPE_TAB_STRIP, + "edge", GTK_POS_TOP, + "stack", priv->stack, + "visible", TRUE, + NULL); + + priv->pinned_button = g_object_new (GTK_TYPE_BUTTON, + "action-name", "panel.pinned", + "child", g_object_new (GTK_TYPE_IMAGE, + "icon-name", "window-maximize-symbolic", + "visible", TRUE, + NULL), + "visible", FALSE, + NULL); + + GTK_CONTAINER_CLASS (dzl_dock_stack_parent_class)->add (GTK_CONTAINER (self), + GTK_WIDGET (priv->tab_strip)); + GTK_CONTAINER_CLASS (dzl_dock_stack_parent_class)->add (GTK_CONTAINER (self), + GTK_WIDGET (priv->stack)); + + dzl_tab_strip_add_control (priv->tab_strip, GTK_WIDGET (priv->pinned_button)); +} + +GtkWidget * +dzl_dock_stack_new (void) +{ + return g_object_new (DZL_TYPE_DOCK_STACK, NULL); +} + +GtkPositionType +dzl_dock_stack_get_edge (DzlDockStack *self) +{ + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_STACK (self), 0); + + return priv->edge; +} + +void +dzl_dock_stack_set_edge (DzlDockStack *self, + GtkPositionType edge) +{ + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_STACK (self)); + g_return_if_fail (edge >= 0); + g_return_if_fail (edge <= 3); + + if (edge != priv->edge) + { + priv->edge = edge; + + dzl_tab_strip_set_edge (priv->tab_strip, edge); + + switch (edge) + { + case GTK_POS_TOP: + gtk_orientable_set_orientation (GTK_ORIENTABLE (self), + GTK_ORIENTATION_VERTICAL); + gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->tab_strip), + GTK_ORIENTATION_HORIZONTAL); + gtk_container_child_set (GTK_CONTAINER (self), GTK_WIDGET (priv->tab_strip), + "position", 0, + NULL); + break; + + case GTK_POS_BOTTOM: + gtk_orientable_set_orientation (GTK_ORIENTABLE (self), + GTK_ORIENTATION_VERTICAL); + gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->tab_strip), + GTK_ORIENTATION_HORIZONTAL); + gtk_container_child_set (GTK_CONTAINER (self), GTK_WIDGET (priv->tab_strip), + "position", 1, + NULL); + break; + + case GTK_POS_LEFT: + gtk_orientable_set_orientation (GTK_ORIENTABLE (self), + GTK_ORIENTATION_HORIZONTAL); + gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->tab_strip), + GTK_ORIENTATION_VERTICAL); + gtk_container_child_set (GTK_CONTAINER (self), GTK_WIDGET (priv->tab_strip), + "position", 0, + NULL); + break; + + case GTK_POS_RIGHT: + gtk_orientable_set_orientation (GTK_ORIENTABLE (self), + GTK_ORIENTATION_HORIZONTAL); + gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->tab_strip), + GTK_ORIENTATION_VERTICAL); + gtk_container_child_set (GTK_CONTAINER (self), GTK_WIDGET (priv->tab_strip), + "position", 1, + NULL); + break; + + default: + g_assert_not_reached (); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EDGE]); + } +} + +static void +dzl_dock_stack_present_child (DzlDockItem *item, + DzlDockItem *child) +{ + DzlDockStack *self = (DzlDockStack *)item; + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + + g_assert (DZL_IS_DOCK_STACK (self)); + g_assert (DZL_IS_DOCK_ITEM (child)); + + gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (child)); +} + +static gboolean +dzl_dock_stack_get_child_visible (DzlDockItem *item, + DzlDockItem *child) +{ + DzlDockStack *self = (DzlDockStack *)item; + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + GtkWidget *visible_child; + + g_assert (DZL_IS_DOCK_STACK (self)); + g_assert (DZL_IS_DOCK_ITEM (child)); + + visible_child = gtk_stack_get_visible_child (priv->stack); + + if (visible_child != NULL) + return gtk_widget_is_ancestor (GTK_WIDGET (child), visible_child); + + return FALSE; +} + +static void +dzl_dock_stack_set_child_visible (DzlDockItem *item, + DzlDockItem *child, + gboolean child_visible) +{ + DzlDockStack *self = (DzlDockStack *)item; + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + GtkWidget *parent; + GtkWidget *last_parent = (GtkWidget *)child; + + g_assert (DZL_IS_DOCK_STACK (self)); + g_assert (DZL_IS_DOCK_ITEM (child)); + + for (parent = gtk_widget_get_parent (GTK_WIDGET (child)); + parent != NULL; + last_parent = parent, parent = gtk_widget_get_parent (parent)) + { + if (parent == (GtkWidget *)priv->stack) + { + gtk_stack_set_visible_child (priv->stack, last_parent); + return; + } + } +} + +static void +update_tab_controls (GtkWidget *widget, + gpointer unused) +{ + g_assert (GTK_IS_WIDGET (widget)); + + if (DZL_IS_TAB (widget)) + _dzl_tab_update_controls (DZL_TAB (widget)); +} + +static void +dzl_dock_stack_update_visibility (DzlDockItem *item) +{ + DzlDockStack *self = (DzlDockStack *)item; + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + + g_assert (DZL_IS_DOCK_STACK (self)); + + gtk_container_foreach (GTK_CONTAINER (priv->tab_strip), + update_tab_controls, + NULL); + + if (!dzl_dock_item_has_widgets (item)) + gtk_widget_hide (GTK_WIDGET (item)); + else + gtk_widget_show (GTK_WIDGET (item)); +} + +static void +dzl_dock_stack_release (DzlDockItem *item, + DzlDockItem *child) +{ + DzlDockStack *self = (DzlDockStack *)item; + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + + g_assert (DZL_IS_DOCK_STACK (self)); + g_assert (DZL_IS_DOCK_ITEM (child)); + + gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (child)); +} + +static void +dzl_dock_stack_init_dock_item_iface (DzlDockItemInterface *iface) +{ + iface->present_child = dzl_dock_stack_present_child; + iface->get_child_visible = dzl_dock_stack_get_child_visible; + iface->set_child_visible = dzl_dock_stack_set_child_visible; + iface->update_visibility = dzl_dock_stack_update_visibility; + iface->release = dzl_dock_stack_release; +} + +gboolean +dzl_dock_stack_get_show_pinned_button (DzlDockStack *self) +{ + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_STACK (self), FALSE); + + return gtk_widget_get_visible (GTK_WIDGET (priv->pinned_button)); +} + +void +dzl_dock_stack_set_show_pinned_button (DzlDockStack *self, + gboolean show_pinned_button) +{ + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_STACK (self)); + + show_pinned_button = !!show_pinned_button; + + if (show_pinned_button != gtk_widget_get_visible (GTK_WIDGET (priv->pinned_button))) + { + gtk_widget_set_visible (GTK_WIDGET (priv->pinned_button), show_pinned_button); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_PINNED_BUTTON]); + } +} + +DzlTabStyle +dzl_dock_stack_get_style (DzlDockStack *self) +{ + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_STACK (self), 0); + + return priv->style; +} + +void +dzl_dock_stack_set_style (DzlDockStack *self, + DzlTabStyle style) +{ + DzlDockStackPrivate *priv = dzl_dock_stack_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_STACK (self)); + + if (priv->style != style) + { + priv->style = style; + dzl_tab_strip_set_style (priv->tab_strip, style); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STYLE]); + } +} diff --git a/src/panel/dzl-dock-stack.h b/src/panel/dzl-dock-stack.h new file mode 100644 index 0000000..a166127 --- /dev/null +++ b/src/panel/dzl-dock-stack.h @@ -0,0 +1,62 @@ +/* dzl-dock-stack.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_STACK_H +#define DZL_DOCK_STACK_H + +#include "dzl-version-macros.h" + +#include "dzl-dock-types.h" + +G_BEGIN_DECLS + +struct _DzlDockStackClass +{ + GtkBoxClass parent; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_dock_stack_new (void); +DZL_AVAILABLE_IN_ALL +GtkPositionType dzl_dock_stack_get_edge (DzlDockStack *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_stack_set_edge (DzlDockStack *self, + GtkPositionType edge); +DZL_AVAILABLE_IN_ALL +DzlTabStyle dzl_dock_stack_get_style (DzlDockStack *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_stack_set_style (DzlDockStack *self, + DzlTabStyle style); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_stack_get_show_pinned_button (DzlDockStack *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_stack_set_show_pinned_button (DzlDockStack *self, + gboolean show_pinned_button); + +G_END_DECLS + +#endif /* DZL_DOCK_STACK_H */ diff --git a/src/panel/dzl-dock-transient-grab.c b/src/panel/dzl-dock-transient-grab.c new file mode 100644 index 0000000..4d9d2cb --- /dev/null +++ b/src/panel/dzl-dock-transient-grab.c @@ -0,0 +1,326 @@ +/* dzl-dock-transient-grab.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock-transient-grab" + +#include "config.h" + +#include "dzl-dock-transient-grab.h" + +struct _DzlDockTransientGrab +{ + GObject parent_instance; + + GPtrArray *items; + GHashTable *hidden; + + guint timeout; + + guint acquired : 1; +}; + +G_DEFINE_TYPE (DzlDockTransientGrab, dzl_dock_transient_grab, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_TIMEOUT, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_dock_transient_grab_weak_notify (gpointer data, + GObject *where_object_was) +{ + DzlDockTransientGrab *self = data; + + g_assert (DZL_IS_DOCK_TRANSIENT_GRAB (self)); + + g_ptr_array_remove (self->items, where_object_was); +} + +static void +dzl_dock_transient_grab_finalize (GObject *object) +{ + DzlDockTransientGrab *self = (DzlDockTransientGrab *)object; + guint i; + + for (i = 0; i < self->items->len; i++) + g_object_weak_unref (g_ptr_array_index (self->items, i), + dzl_dock_transient_grab_weak_notify, + self); + + g_clear_pointer (&self->items, g_ptr_array_unref); + g_clear_pointer (&self->hidden, g_hash_table_unref); + + G_OBJECT_CLASS (dzl_dock_transient_grab_parent_class)->finalize (object); +} + +static void +dzl_dock_transient_grab_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlDockTransientGrab *self = DZL_DOCK_TRANSIENT_GRAB (object); + + switch (prop_id) + { + case PROP_TIMEOUT: + g_value_set_uint (value, dzl_dock_transient_grab_get_timeout (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_transient_grab_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlDockTransientGrab *self = DZL_DOCK_TRANSIENT_GRAB (object); + + switch (prop_id) + { + case PROP_TIMEOUT: + dzl_dock_transient_grab_set_timeout (self, g_value_get_uint (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_transient_grab_class_init (DzlDockTransientGrabClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_dock_transient_grab_finalize; + object_class->get_property = dzl_dock_transient_grab_get_property; + object_class->set_property = dzl_dock_transient_grab_set_property; + + properties [PROP_TIMEOUT] = + g_param_spec_uint ("timeout", + "Timeout", + "Timeout", + 0, + G_MAXUINT, + 0, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_dock_transient_grab_init (DzlDockTransientGrab *self) +{ + self->items = g_ptr_array_new (); + self->hidden = g_hash_table_new (NULL, NULL); +} + +DzlDockTransientGrab * +dzl_dock_transient_grab_new (void) +{ + return g_object_new (DZL_TYPE_DOCK_TRANSIENT_GRAB, NULL); +} + +guint +dzl_dock_transient_grab_get_timeout (DzlDockTransientGrab *self) +{ + g_return_val_if_fail (DZL_IS_DOCK_TRANSIENT_GRAB (self), 0); + + return self->timeout; +} + +void +dzl_dock_transient_grab_set_timeout (DzlDockTransientGrab *self, + guint timeout) +{ + g_return_if_fail (DZL_IS_DOCK_TRANSIENT_GRAB (self)); + + if (timeout != self->timeout) + { + self->timeout = timeout; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TIMEOUT]); + } +} + +gboolean +dzl_dock_transient_grab_contains (DzlDockTransientGrab *self, + DzlDockItem *item) +{ + guint i; + + g_return_val_if_fail (DZL_IS_DOCK_TRANSIENT_GRAB (self), FALSE); + g_return_val_if_fail (DZL_IS_DOCK_ITEM (item), FALSE); + + for (i = 0; i < self->items->len; i++) + if (g_ptr_array_index (self->items, i) == item) + return TRUE; + + return FALSE; +} + +void +dzl_dock_transient_grab_add_item (DzlDockTransientGrab *self, + DzlDockItem *item) +{ + g_return_if_fail (DZL_IS_DOCK_TRANSIENT_GRAB (self)); + g_return_if_fail (DZL_IS_DOCK_ITEM (item)); + + g_ptr_array_add (self->items, item); + + g_object_weak_ref (G_OBJECT (item), + dzl_dock_transient_grab_weak_notify, + self); +} + +static void +dzl_dock_transient_grab_remove_index (DzlDockTransientGrab *self, + guint index) +{ + DzlDockItem *item; + + g_return_if_fail (DZL_IS_DOCK_TRANSIENT_GRAB (self)); + g_return_if_fail (index < self->items->len); + + item = g_ptr_array_index (self->items, index); + g_object_weak_unref (G_OBJECT (item), + dzl_dock_transient_grab_weak_notify, + self); + g_ptr_array_remove_index (self->items, index); + g_hash_table_remove (self->hidden, item); +} + +void +dzl_dock_transient_grab_remove_item (DzlDockTransientGrab *self, + DzlDockItem *item) +{ + guint i; + + g_return_if_fail (DZL_IS_DOCK_TRANSIENT_GRAB (self)); + g_return_if_fail (DZL_IS_DOCK_ITEM (item)); + + for (i = 0; i < self->items->len; i++) + { + DzlDockItem *iter = g_ptr_array_index (self->items, i); + + if (item == iter) + { + dzl_dock_transient_grab_remove_index (self, i); + return; + } + } +} + +void +dzl_dock_transient_grab_acquire (DzlDockTransientGrab *self) +{ + guint i; + + g_return_if_fail (DZL_IS_DOCK_TRANSIENT_GRAB (self)); + g_return_if_fail (self->acquired == FALSE); + + self->acquired = TRUE; + + for (i = self->items->len; i > 1; i--) + { + DzlDockItem *parent = g_ptr_array_index (self->items, i - 1); + DzlDockItem *child = g_ptr_array_index (self->items, i - 2); + + if (!dzl_dock_item_get_child_visible (parent, child)) + { + dzl_dock_item_set_child_visible (parent, child, TRUE); + g_hash_table_insert (self->hidden, child, NULL); + } + } +} + +void +dzl_dock_transient_grab_release (DzlDockTransientGrab *self) +{ + guint i; + + g_return_if_fail (DZL_IS_DOCK_TRANSIENT_GRAB (self)); + g_return_if_fail (self->acquired == TRUE); + + for (i = 0; i < self->items->len; i++) + { + DzlDockItem *item = g_ptr_array_index (self->items, i); + + if (g_hash_table_contains (self->hidden, item)) + { + DzlDockItem *parent = dzl_dock_item_get_parent (item); + + if (parent != NULL) + dzl_dock_item_set_child_visible (parent, item, FALSE); + } + } +} + +gboolean +dzl_dock_transient_grab_is_descendant (DzlDockTransientGrab *self, + GtkWidget *widget) +{ + g_return_val_if_fail (DZL_IS_DOCK_TRANSIENT_GRAB (self), FALSE); + + if (self->items->len > 0) + { + GtkWidget *item = g_ptr_array_index (self->items, 0); + GtkWidget *ancestor; + + ancestor = gtk_widget_get_ancestor (widget, DZL_TYPE_DOCK_ITEM); + + return (item == ancestor); + } + + return FALSE; +} + +void +dzl_dock_transient_grab_steal_common_ancestors (DzlDockTransientGrab *self, + DzlDockTransientGrab *other) +{ + guint i; + + g_return_if_fail (DZL_IS_DOCK_TRANSIENT_GRAB (self)); + g_return_if_fail (DZL_IS_DOCK_TRANSIENT_GRAB (other)); + + for (i = other->items->len; i > 0; i--) + { + DzlDockItem *item = g_ptr_array_index (other->items, i - 1); + + if (dzl_dock_transient_grab_contains (self, item)) + { + /* + * Since we are stealing the common ancestors, we don't want the + * previous grab to hide them when releasing, so clear the items + * from the hash of children it wants to hide. + */ + g_hash_table_remove (other->hidden, item); + + dzl_dock_transient_grab_add_item (self, item); + dzl_dock_transient_grab_remove_index (other, i - 1); + } + } +} diff --git a/src/panel/dzl-dock-transient-grab.h b/src/panel/dzl-dock-transient-grab.h new file mode 100644 index 0000000..fdf1b4f --- /dev/null +++ b/src/panel/dzl-dock-transient-grab.h @@ -0,0 +1,67 @@ +/* dzl-dock-transient-grab.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_TRANSIENT_GRAB_H +#define DZL_DOCK_TRANSIENT_GRAB_H + +#include "dzl-version-macros.h" + +#include "dzl-dock-item.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_DOCK_TRANSIENT_GRAB (dzl_dock_transient_grab_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlDockTransientGrab, dzl_dock_transient_grab, DZL, DOCK_TRANSIENT_GRAB, GObject) + +DZL_AVAILABLE_IN_ALL +DzlDockTransientGrab *dzl_dock_transient_grab_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_dock_transient_grab_add_item (DzlDockTransientGrab *self, + DzlDockItem *item); +DZL_AVAILABLE_IN_ALL +void dzl_dock_transient_grab_remove_item (DzlDockTransientGrab *self, + DzlDockItem *item); +DZL_AVAILABLE_IN_ALL +void dzl_dock_transient_grab_acquire (DzlDockTransientGrab *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_transient_grab_release (DzlDockTransientGrab *self); +DZL_AVAILABLE_IN_ALL +guint dzl_dock_transient_grab_get_timeout (DzlDockTransientGrab *self); +DZL_AVAILABLE_IN_ALL +void dzl_dock_transient_grab_set_timeout (DzlDockTransientGrab *self, + guint timeout); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_transient_grab_contains (DzlDockTransientGrab *self, + DzlDockItem *item); +DZL_AVAILABLE_IN_ALL +gboolean dzl_dock_transient_grab_is_descendant (DzlDockTransientGrab *self, + GtkWidget *widget); +DZL_AVAILABLE_IN_ALL +void dzl_dock_transient_grab_steal_common_ancestors (DzlDockTransientGrab *self, + DzlDockTransientGrab *other); + +G_END_DECLS + +#endif /* DZL_DOCK_TRANSIENT_GRAB_H */ + diff --git a/src/panel/dzl-dock-types.h b/src/panel/dzl-dock-types.h new file mode 100644 index 0000000..6a349cb --- /dev/null +++ b/src/panel/dzl-dock-types.h @@ -0,0 +1,93 @@ +/* dzl-dock-types.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_TYPES_H +#define DZL_TYPES_H + +#include + +#include "dzl-version-macros.h" + +#include "widgets/dzl-bin.h" +#include "widgets/dzl-multi-paned.h" +#include "panel/dzl-dock-revealer.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_DOCK (dzl_dock_get_type ()) +#define DZL_TYPE_DOCK_BIN (dzl_dock_bin_get_type()) +#define DZL_TYPE_DOCK_BIN_EDGE (dzl_dock_bin_edge_get_type()) +#define DZL_TYPE_DOCK_ITEM (dzl_dock_item_get_type()) +#define DZL_TYPE_DOCK_MANAGER (dzl_dock_manager_get_type()) +#define DZL_TYPE_DOCK_OVERLAY (dzl_dock_overlay_get_type()) +#define DZL_TYPE_DOCK_OVERLAY_EDGE (dzl_dock_overlay_edge_get_type()) +#define DZL_TYPE_DOCK_PANED (dzl_dock_paned_get_type()) +#define DZL_TYPE_DOCK_STACK (dzl_dock_stack_get_type()) +#define DZL_TYPE_DOCK_TAB_STRIP (dzl_dock_tab_strip_get_type()) +#define DZL_TYPE_DOCK_WIDGET (dzl_dock_widget_get_type()) +#define DZL_TYPE_DOCK_WINDOW (dzl_dock_window_get_type()) +#define DZL_TYPE_TAB (dzl_tab_get_type()) +#define DZL_TYPE_TAB_STRIP (dzl_tab_strip_get_type()) +#define DZL_TYPE_TAB_STYLE (dzl_tab_style_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlDockBin, dzl_dock_bin, DZL, DOCK_BIN, GtkContainer) +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlDockBinEdge, dzl_dock_bin_edge, DZL, DOCK_BIN_EDGE, DzlDockRevealer) +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlDockManager, dzl_dock_manager, DZL, DOCK_MANAGER, GObject) +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlDockOverlay, dzl_dock_overlay, DZL, DOCK_OVERLAY, GtkEventBox) +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlDockPaned, dzl_dock_paned, DZL, DOCK_PANED, DzlMultiPaned) +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlDockStack, dzl_dock_stack, DZL, DOCK_STACK, GtkBox) +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlDockWidget, dzl_dock_widget, DZL, DOCK_WIDGET, DzlBin) +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlDockWindow, dzl_dock_window, DZL, DOCK_WINDOW, GtkWindow) +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlTabStrip, dzl_tab_strip, DZL, TAB_STRIP, GtkBox) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlTab, dzl_tab, DZL, TAB, DzlBin) +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlDockOverlayEdge, dzl_dock_overlay_edge, DZL, DOCK_OVERLAY_EDGE, DzlBin) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_INTERFACE (DzlDock, dzl_dock, DZL, DOCK, GtkContainer) +DZL_AVAILABLE_IN_ALL +G_DECLARE_INTERFACE (DzlDockItem, dzl_dock_item, DZL, DOCK_ITEM, GtkWidget) + +typedef enum +{ + DZL_TAB_TEXT = 1 << 0, + DZL_TAB_ICONS = 1 << 1, + DZL_TAB_BOTH = (DZL_TAB_TEXT | DZL_TAB_ICONS), +} DzlTabStyle; + +DZL_AVAILABLE_IN_ALL +GType dzl_tab_style_get_type (void); + +G_END_DECLS + +#endif /* DZL_TYPES_H */ diff --git a/src/panel/dzl-dock-widget.c b/src/panel/dzl-dock-widget.c new file mode 100644 index 0000000..3fd12b5 --- /dev/null +++ b/src/panel/dzl-dock-widget.c @@ -0,0 +1,287 @@ +/* dzl-dock-widget.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock-widget" + +#include "config.h" + +#include "panel/dzl-dock-item.h" +#include "panel/dzl-dock-widget.h" +#include "util/dzl-util-private.h" + +typedef struct +{ + gchar *title; + gchar *icon_name; + guint can_close : 1; +} DzlDockWidgetPrivate; + +static void dock_item_iface_init (DzlDockItemInterface *iface); + +G_DEFINE_TYPE_EXTENDED (DzlDockWidget, dzl_dock_widget, DZL_TYPE_BIN, 0, + G_ADD_PRIVATE (DzlDockWidget) + G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK_ITEM, dock_item_iface_init)) + +enum { + PROP_0, + PROP_CAN_CLOSE, + PROP_ICON_NAME, + PROP_MANAGER, + PROP_TITLE, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static gchar * +dzl_dock_widget_item_get_title (DzlDockItem *item) +{ + DzlDockWidget *self = (DzlDockWidget *)item; + DzlDockWidgetPrivate *priv = dzl_dock_widget_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_WIDGET (self), NULL); + + return g_strdup (priv->title); +} + +static gchar * +dzl_dock_widget_item_get_icon_name (DzlDockItem *item) +{ + DzlDockWidget *self = (DzlDockWidget *)item; + DzlDockWidgetPrivate *priv = dzl_dock_widget_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_WIDGET (self), NULL); + + return g_strdup (priv->icon_name); +} + +static gboolean +dzl_dock_widget_get_can_close (DzlDockItem *item) +{ + DzlDockWidget *self = (DzlDockWidget *)item; + DzlDockWidgetPrivate *priv = dzl_dock_widget_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_DOCK_WIDGET (self), FALSE); + + return priv->can_close; +} + +static void +dzl_dock_widget_set_can_close (DzlDockWidget *self, + gboolean can_close) +{ + DzlDockWidgetPrivate *priv = dzl_dock_widget_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_WIDGET (self)); + + can_close = !!can_close; + + if (can_close != priv->can_close) + { + priv->can_close = can_close; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CAN_CLOSE]); + } +} + +static void +dzl_dock_widget_grab_focus (GtkWidget *widget) +{ + DzlDockWidget *self = (DzlDockWidget *)widget; + GtkWidget *child; + + g_assert (DZL_IS_DOCK_WIDGET (self)); + + dzl_dock_item_present (DZL_DOCK_ITEM (self)); + + child = gtk_bin_get_child (GTK_BIN (self)); + + if (child == NULL || !gtk_widget_child_focus (child, GTK_DIR_TAB_FORWARD)) + GTK_WIDGET_CLASS (dzl_dock_widget_parent_class)->grab_focus (GTK_WIDGET (self)); +} + +static void +dzl_dock_widget_finalize (GObject *object) +{ + DzlDockWidget *self = (DzlDockWidget *)object; + DzlDockWidgetPrivate *priv = dzl_dock_widget_get_instance_private (self); + + g_clear_pointer (&priv->title, g_free); + g_clear_pointer (&priv->icon_name, g_free); + + G_OBJECT_CLASS (dzl_dock_widget_parent_class)->finalize (object); +} + +static void +dzl_dock_widget_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlDockWidget *self = DZL_DOCK_WIDGET (object); + + switch (prop_id) + { + case PROP_CAN_CLOSE: + g_value_set_boolean (value, dzl_dock_widget_get_can_close (DZL_DOCK_ITEM (self))); + break; + + case PROP_ICON_NAME: + g_value_take_string (value, dzl_dock_widget_item_get_icon_name (DZL_DOCK_ITEM (self))); + break; + + case PROP_MANAGER: + g_value_set_object (value, dzl_dock_item_get_manager (DZL_DOCK_ITEM (self))); + break; + + case PROP_TITLE: + g_value_take_string (value, dzl_dock_widget_item_get_title (DZL_DOCK_ITEM (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_widget_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlDockWidget *self = DZL_DOCK_WIDGET (object); + + switch (prop_id) + { + case PROP_CAN_CLOSE: + dzl_dock_widget_set_can_close (self, g_value_get_boolean (value)); + break; + + case PROP_ICON_NAME: + dzl_dock_widget_set_icon_name (self, g_value_get_string (value)); + break; + + case PROP_MANAGER: + dzl_dock_item_set_manager (DZL_DOCK_ITEM (self), g_value_get_object (value)); + break; + + case PROP_TITLE: + dzl_dock_widget_set_title (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_widget_class_init (DzlDockWidgetClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_dock_widget_finalize; + object_class->get_property = dzl_dock_widget_get_property; + object_class->set_property = dzl_dock_widget_set_property; + + widget_class->grab_focus = dzl_dock_widget_grab_focus; + + properties [PROP_CAN_CLOSE] = + g_param_spec_boolean ("can-close", + "Can Close", + "If the dock widget can be closed by the user", + FALSE, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "Icon Name", + "Icon Name", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_MANAGER] = + g_param_spec_object ("manager", + "Manager", + "The panel manager", + DZL_TYPE_DOCK_MANAGER, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "Title", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_css_name (widget_class, "dzldockwidget"); +} + +static void +dzl_dock_widget_init (DzlDockWidget *self) +{ + gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); + gtk_widget_set_can_focus (GTK_WIDGET (self), TRUE); +} + +GtkWidget * +dzl_dock_widget_new (void) +{ + return g_object_new (DZL_TYPE_DOCK_WIDGET, NULL); +} + +void +dzl_dock_widget_set_title (DzlDockWidget *self, + const gchar *title) +{ + DzlDockWidgetPrivate *priv = dzl_dock_widget_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_WIDGET (self)); + + if (g_strcmp0 (title, priv->title) != 0) + { + g_free (priv->title); + priv->title = g_strdup (title); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]); + } +} + +void +dzl_dock_widget_set_icon_name (DzlDockWidget *self, + const gchar *icon_name) +{ + DzlDockWidgetPrivate *priv = dzl_dock_widget_get_instance_private (self); + + g_return_if_fail (DZL_IS_DOCK_WIDGET (self)); + + if (g_strcmp0 (icon_name, priv->icon_name) != 0) + { + g_free (priv->icon_name); + priv->icon_name = g_strdup (icon_name); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]); + } +} + +static void +dock_item_iface_init (DzlDockItemInterface *iface) +{ + iface->get_can_close = dzl_dock_widget_get_can_close; + iface->get_title = dzl_dock_widget_item_get_title; + iface->get_icon_name = dzl_dock_widget_item_get_icon_name; +} diff --git a/src/panel/dzl-dock-widget.h b/src/panel/dzl-dock-widget.h new file mode 100644 index 0000000..069fce0 --- /dev/null +++ b/src/panel/dzl-dock-widget.h @@ -0,0 +1,57 @@ +/* dzl-dock-widget.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_WIDGET_H +#define DZL_DOCK_WIDGET_H + +#include "dzl-version-macros.h" + +#include "dzl-dock-types.h" + +G_BEGIN_DECLS + +struct _DzlDockWidgetClass +{ + DzlBinClass parent; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_dock_widget_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_dock_widget_set_title (DzlDockWidget *self, + const gchar *title); +DZL_AVAILABLE_IN_ALL +void dzl_dock_widget_set_icon_name (DzlDockWidget *self, + const gchar *icon_name); + +G_END_DECLS + +#endif /* DZL_DOCK_WIDGET_H */ diff --git a/src/panel/dzl-dock-window.c b/src/panel/dzl-dock-window.c new file mode 100644 index 0000000..7b78639 --- /dev/null +++ b/src/panel/dzl-dock-window.c @@ -0,0 +1,117 @@ +/* dzl-dock-window.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock-window" + +#include "config.h" + +#include "dzl-dock-item.h" +#include "dzl-dock-window.h" + +typedef struct +{ + void *foo; +} DzlDockWindowPrivate; + +static void dzl_dock_window_init_dock_iface (DzlDockInterface *iface); + +G_DEFINE_TYPE_EXTENDED (DzlDockWindow, dzl_dock_window, GTK_TYPE_WINDOW, 0, + G_ADD_PRIVATE (DzlDockWindow) + G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK_ITEM, NULL) + G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK, dzl_dock_window_init_dock_iface)) + +enum { + PROP_0, + PROP_MANAGER, + N_PROPS +}; + +static void +dzl_dock_window_finalize (GObject *object) +{ + G_OBJECT_CLASS (dzl_dock_window_parent_class)->finalize (object); +} + +static void +dzl_dock_window_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlDockWindow *self = DZL_DOCK_WINDOW (object); + + switch (prop_id) + { + case PROP_MANAGER: + g_value_set_object (value, dzl_dock_item_get_manager (DZL_DOCK_ITEM (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_window_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlDockWindow *self = DZL_DOCK_WINDOW (object); + + switch (prop_id) + { + case PROP_MANAGER: + dzl_dock_item_set_manager (DZL_DOCK_ITEM (self), g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_dock_window_class_init (DzlDockWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_dock_window_finalize; + object_class->get_property = dzl_dock_window_get_property; + object_class->set_property = dzl_dock_window_set_property; + + g_object_class_override_property (object_class, PROP_MANAGER, "manager"); + + gtk_widget_class_set_css_name (widget_class, "dzldockwindow"); +} + +static void +dzl_dock_window_init (DzlDockWindow *self) +{ +} + +GtkWidget * +dzl_dock_window_new (void) +{ + return g_object_new (DZL_TYPE_DOCK_WINDOW, NULL); +} + +static void +dzl_dock_window_init_dock_iface (DzlDockInterface *iface) +{ +} diff --git a/src/panel/dzl-dock-window.h b/src/panel/dzl-dock-window.h new file mode 100644 index 0000000..03e4cda --- /dev/null +++ b/src/panel/dzl-dock-window.h @@ -0,0 +1,51 @@ +/* dzl-dock-window.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_WINDOW_H +#define DZL_DOCK_WINDOW_H + +#include "dzl-version-macros.h" + +#include "dzl-dock.h" + +G_BEGIN_DECLS + +struct _DzlDockWindowClass +{ + GtkWindowClass parent; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_dock_window_new (void); + +G_END_DECLS + +#endif /* DZL_DOCK_WINDOW_H */ diff --git a/src/panel/dzl-dock.c b/src/panel/dzl-dock.c new file mode 100644 index 0000000..e2b2ebd --- /dev/null +++ b/src/panel/dzl-dock.c @@ -0,0 +1,47 @@ +/* dzl-dock.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dock" + +#include "config.h" + +#include "dzl-dock.h" +#include "dzl-resources.h" + +G_DEFINE_INTERFACE (DzlDock, dzl_dock, GTK_TYPE_CONTAINER) + +static void +dzl_dock_default_init (DzlDockInterface *iface) +{ + GdkScreen *screen; + + g_resources_register (dzl_get_resource ()); + + screen = gdk_screen_get_default (); + + if (screen != NULL) + gtk_icon_theme_add_resource_path (gtk_icon_theme_get_default (), + "/org/gnome/dazzle/icons"); + + g_object_interface_install_property (iface, + g_param_spec_object ("manager", + "Manager", + "Manager", + DZL_TYPE_DOCK_MANAGER, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); +} diff --git a/src/panel/dzl-dock.h b/src/panel/dzl-dock.h new file mode 100644 index 0000000..d3da0aa --- /dev/null +++ b/src/panel/dzl-dock.h @@ -0,0 +1,37 @@ +/* dzl-dock.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_DOCK_H +#define DZL_DOCK_H + +#include "dzl-dock-types.h" + +G_BEGIN_DECLS + +struct _DzlDockInterface +{ + GTypeInterface parent; +}; + +G_END_DECLS + +#endif /* DZL_DOCK_H */ diff --git a/src/panel/dzl-tab-private.h b/src/panel/dzl-tab-private.h new file mode 100644 index 0000000..168d75c --- /dev/null +++ b/src/panel/dzl-tab-private.h @@ -0,0 +1,30 @@ +/* dzl-tab-private.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_TAB_PRIVATE_H +#define DZL_TAB_PRIVATE_H + +#include "dzl-tab.h" + +G_BEGIN_DECLS + +void _dzl_tab_update_controls (DzlTab *self); + +G_END_DECLS + +#endif /* DZL_TAB_PRIVATE_H */ diff --git a/src/panel/dzl-tab-strip.c b/src/panel/dzl-tab-strip.c new file mode 100644 index 0000000..6c973f3 --- /dev/null +++ b/src/panel/dzl-tab-strip.c @@ -0,0 +1,763 @@ +/* dzl-tab-strip.c + * + * Copyright (C) 2016 Christian Hergert + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define G_LOG_DOMAIN "dzl-tab-strip" + +#include "config.h" + +#include "dzl-dock-item.h" +#include "dzl-dock-widget.h" +#include "dzl-tab.h" +#include "dzl-tab-strip.h" + +typedef struct +{ + GAction *action; + GtkStack *stack; + GtkPositionType edge : 2; + DzlTabStyle style : 2; +} DzlTabStripPrivate; + +static void buildable_iface_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (DzlTabStrip, dzl_tab_strip, GTK_TYPE_BOX, + G_ADD_PRIVATE (DzlTabStrip) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init)) + +enum { + PROP_0, + PROP_EDGE, + PROP_STACK, + PROP_STYLE, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static void +set_tab_state (GSimpleAction *action, + GVariant *state, + gpointer user_data) +{ + DzlTabStrip *self = user_data; + DzlTabStripPrivate *priv = dzl_tab_strip_get_instance_private (self); + GtkWidget *nth_child; + DzlTab *tab = NULL; + GList *list; + gint stateval; + + g_assert (G_IS_SIMPLE_ACTION (action)); + g_assert (DZL_IS_TAB_STRIP (self)); + g_assert (state != NULL); + g_assert (g_variant_is_of_type (state, G_VARIANT_TYPE_INT32)); + + g_simple_action_set_state (action, state); + + stateval = g_variant_get_int32 (state); + + list = gtk_container_get_children (GTK_CONTAINER (priv->stack)); + nth_child = g_list_nth_data (list, stateval); + g_clear_pointer (&list, g_list_free); + + if (nth_child != NULL) + { + tab = g_object_get_data (G_OBJECT (nth_child), "DZL_TAB"); + gtk_stack_set_visible_child (priv->stack, nth_child); + /* + * When clicking an active toggle button, we get the state callback but then + * the toggle button disables the checked state. So ensure it stays on by + * manually setting the state. + */ + if (DZL_IS_TAB (tab)) + dzl_tab_set_active (DZL_TAB (tab), TRUE); + } +} + +static void +dzl_tab_strip_update_action_targets (DzlTabStrip *self) +{ + const GList *iter; + GList *list; + gint i; + + g_assert (DZL_IS_TAB_STRIP (self)); + + list = gtk_container_get_children (GTK_CONTAINER (self)); + + for (i = 0, iter = list; iter != NULL; iter = iter->next, i++) + { + GtkWidget *widget = iter->data; + + /* Ignore controls, and just update tabs */ + if (DZL_IS_TAB (widget)) + gtk_actionable_set_action_target (GTK_ACTIONABLE (widget), "i", i); + } + + g_list_free (list); +} + +static void +dzl_tab_strip_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlTabStrip *self = (DzlTabStrip *)container; + DzlTabStripPrivate *priv = dzl_tab_strip_get_instance_private (self); + + g_assert (DZL_IS_TAB_STRIP (self)); + g_assert (GTK_IS_WIDGET (widget)); + + if (DZL_IS_TAB (widget)) + dzl_tab_set_edge (DZL_TAB (widget), priv->edge); + + GTK_CONTAINER_CLASS (dzl_tab_strip_parent_class)->add (container, widget); + + dzl_tab_strip_update_action_targets (self); +} + +static void +dzl_tab_strip_remove (GtkContainer *container, + GtkWidget *widget) +{ + DzlTabStrip *self = (DzlTabStrip *)container; + + g_assert (DZL_IS_TAB_STRIP (self)); + g_assert (GTK_IS_WIDGET (widget)); + + GTK_CONTAINER_CLASS (dzl_tab_strip_parent_class)->remove (container, widget); + + dzl_tab_strip_update_action_targets (self); +} + +static void +dzl_tab_strip_destroy (GtkWidget *widget) +{ + DzlTabStrip *self = (DzlTabStrip *)widget; + DzlTabStripPrivate *priv = dzl_tab_strip_get_instance_private (self); + + g_assert (DZL_IS_TAB_STRIP (self)); + + gtk_widget_insert_action_group (GTK_WIDGET (self), "tab-strip", NULL); + + dzl_tab_strip_set_stack (self, NULL); + + g_clear_object (&priv->action); + g_clear_object (&priv->stack); + + GTK_WIDGET_CLASS (dzl_tab_strip_parent_class)->destroy (widget); +} + +static void +dzl_tab_strip_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlTabStrip *self = DZL_TAB_STRIP (object); + + switch (prop_id) + { + case PROP_EDGE: + g_value_set_enum (value, dzl_tab_strip_get_edge (self)); + break; + + case PROP_STACK: + g_value_set_object (value, dzl_tab_strip_get_stack (self)); + break; + + case PROP_STYLE: + g_value_set_flags (value, dzl_tab_strip_get_style (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_tab_strip_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlTabStrip *self = DZL_TAB_STRIP (object); + + switch (prop_id) + { + case PROP_EDGE: + dzl_tab_strip_set_edge (self, g_value_get_enum (value)); + break; + + case PROP_STACK: + dzl_tab_strip_set_stack (self, g_value_get_object (value)); + break; + + case PROP_STYLE: + dzl_tab_strip_set_style (self, g_value_get_flags (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_tab_strip_class_init (DzlTabStripClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->get_property = dzl_tab_strip_get_property; + object_class->set_property = dzl_tab_strip_set_property; + + widget_class->destroy = dzl_tab_strip_destroy; + + container_class->add = dzl_tab_strip_add; + container_class->remove = dzl_tab_strip_remove; + + properties [PROP_EDGE] = + g_param_spec_enum ("edge", + "Edge", + "The edge for the tab-strip", + GTK_TYPE_POSITION_TYPE, + GTK_POS_TOP, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_STYLE] = + g_param_spec_flags ("style", + "Style", + "The tab style", + DZL_TYPE_TAB_STYLE, + DZL_TAB_BOTH, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_STACK] = + g_param_spec_object ("stack", + "Stack", + "The stack of items to manage.", + GTK_TYPE_STACK, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_css_name (widget_class, "dzltabstrip"); +} + +static void +dzl_tab_strip_init (DzlTabStrip *self) +{ + DzlTabStripPrivate *priv = dzl_tab_strip_get_instance_private (self); + GSimpleActionGroup *group; + static const GActionEntry entries[] = { + { "tab", NULL, "i", "0", set_tab_state }, + }; + + priv->style = DZL_TAB_BOTH; + + gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_HORIZONTAL); + + group = g_simple_action_group_new (); + g_action_map_add_action_entries (G_ACTION_MAP (group), entries, G_N_ELEMENTS (entries), self); + priv->action = g_object_ref (g_action_map_lookup_action (G_ACTION_MAP (group), "tab")); + gtk_widget_insert_action_group (GTK_WIDGET (self), "tab-strip", G_ACTION_GROUP (group)); + g_object_unref (group); + + dzl_tab_strip_set_edge (self, GTK_POS_TOP); +} + +static void +dzl_tab_strip_child_position_changed (DzlTabStrip *self, + GParamSpec *pspec, + GtkWidget *child) +{ + GVariant *state; + GtkWidget *parent; + DzlTab *tab; + gint position = -1; + + g_assert (DZL_IS_TAB_STRIP (self)); + g_assert (GTK_IS_WIDGET (child)); + + tab = g_object_get_data (G_OBJECT (child), "DZL_TAB"); + + if (!tab || !DZL_IS_TAB (tab)) + { + g_warning ("Child %s (%p) is missing backpointer to tab", + G_OBJECT_TYPE_NAME (child), child); + return; + } + + parent = gtk_widget_get_parent (child); + + g_assert (GTK_IS_STACK (parent)); + + gtk_container_child_get (GTK_CONTAINER (parent), child, + "position", &position, + NULL); + + if (position < 0) + { + g_warning ("Improbable position for child, %d", position); + return; + } + + gtk_container_child_set (GTK_CONTAINER (self), GTK_WIDGET (tab), + "position", position, + NULL); + + state = g_variant_new_int32 (position); + gtk_actionable_set_action_target_value (GTK_ACTIONABLE (tab), state); + + dzl_tab_strip_update_action_targets (self); +} + +static void +dzl_tab_strip_child_title_changed (DzlTabStrip *self, + GParamSpec *pspec, + GtkWidget *child) +{ + gchar *title = NULL; + GtkWidget *parent; + DzlTab *tab; + + g_assert (DZL_IS_TAB_STRIP (self)); + g_assert (GTK_IS_WIDGET (child)); + + tab = g_object_get_data (G_OBJECT (child), "DZL_TAB"); + + if (!DZL_IS_TAB (tab)) + return; + + parent = gtk_widget_get_parent (child); + + g_assert (GTK_IS_STACK (parent)); + + gtk_container_child_get (GTK_CONTAINER (parent), child, + "title", &title, + NULL); + + dzl_tab_set_title (tab, title); + + g_free (title); +} + +static void +dzl_tab_strip_child_icon_name_changed (DzlTabStrip *self, + GParamSpec *pspec, + GtkWidget *child) +{ + gchar *icon_name = NULL; + GtkWidget *parent; + DzlTab *tab; + + g_assert (DZL_IS_TAB_STRIP (self)); + g_assert (GTK_IS_WIDGET (child)); + + tab = g_object_get_data (G_OBJECT (child), "DZL_TAB"); + + if (!DZL_IS_TAB (tab)) + return; + + parent = gtk_widget_get_parent (child); + + g_assert (GTK_IS_STACK (parent)); + + gtk_container_child_get (GTK_CONTAINER (parent), child, + "icon-name", &icon_name, + NULL); + + dzl_tab_set_icon_name (tab, icon_name); + + g_free (icon_name); +} + +static void +dzl_tab_strip_stack_notify_visible_child (DzlTabStrip *self, + GParamSpec *pspec, + GtkStack *stack) +{ + GtkWidget *visible; + + g_assert (DZL_IS_TAB_STRIP (self)); + g_assert (GTK_IS_STACK (stack)); + + visible = gtk_stack_get_visible_child (stack); + + if (visible != NULL) + { + DzlTab *tab = g_object_get_data (G_OBJECT (visible), "DZL_TAB"); + + if (DZL_IS_TAB (tab)) + dzl_tab_set_active (DZL_TAB (tab), TRUE); + } +} + +static void +dzl_tab_strip_tab_clicked (DzlTabStrip *self, + DzlTab *tab) +{ + GtkWidget *widget; + + g_assert (DZL_IS_TAB_STRIP (self)); + g_assert (DZL_IS_TAB (tab)); + + if (NULL != (widget = dzl_tab_get_widget (tab))) + { + if (dzl_tab_get_active (tab)) + gtk_widget_grab_focus (widget); + } +} + +static void +dzl_tab_strip_stack_add (DzlTabStrip *self, + GtkWidget *widget, + GtkStack *stack) +{ + DzlTabStripPrivate *priv = dzl_tab_strip_get_instance_private (self); + g_autoptr(GVariant) target = g_variant_ref_sink (g_variant_new_int32 (0)); + DzlTab *tab; + gboolean can_close = FALSE; + + g_assert (DZL_IS_TAB_STRIP (self)); + g_assert (GTK_IS_WIDGET (widget)); + g_assert (GTK_IS_STACK (stack)); + + if (DZL_IS_DOCK_ITEM (widget)) + can_close = dzl_dock_item_get_can_close (DZL_DOCK_ITEM (widget)); + + tab = g_object_new (DZL_TYPE_TAB, + "action-name", "tab-strip.tab", + "action-target", target, + "can-close", can_close, + "edge", priv->edge, + "style", priv->style, + "widget", widget, + NULL); + + g_object_set_data (G_OBJECT (widget), "DZL_TAB", tab); + + g_signal_connect_object (tab, + "clicked", + G_CALLBACK (dzl_tab_strip_tab_clicked), + self, + G_CONNECT_SWAPPED | G_CONNECT_AFTER); + + g_signal_connect_object (widget, + "child-notify::position", + G_CALLBACK (dzl_tab_strip_child_position_changed), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (widget, + "child-notify::title", + G_CALLBACK (dzl_tab_strip_child_title_changed), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (widget, + "child-notify::icon-name", + G_CALLBACK (dzl_tab_strip_child_icon_name_changed), + self, + G_CONNECT_SWAPPED); + + gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (tab), + "pack-type", GTK_PACK_START, + "expand", TRUE, + "fill", TRUE, + NULL); + + g_object_bind_property (widget, "visible", tab, "visible", G_BINDING_SYNC_CREATE); + + if (DZL_IS_DOCK_WIDGET (widget)) + g_object_bind_property (widget, "can-close", tab, "can-close", 0); + + dzl_tab_strip_child_title_changed (self, NULL, widget); + dzl_tab_strip_child_icon_name_changed (self, NULL, widget); + dzl_tab_strip_stack_notify_visible_child (self, NULL, stack); +} + +static void +dzl_tab_strip_stack_remove (DzlTabStrip *self, + GtkWidget *widget, + GtkStack *stack) +{ + DzlTab *tab; + + g_assert (DZL_IS_TAB_STRIP (self)); + g_assert (GTK_IS_WIDGET (widget)); + g_assert (GTK_IS_STACK (stack)); + + tab = g_object_get_data (G_OBJECT (widget), "DZL_TAB"); + + if (DZL_IS_TAB (tab)) + { + g_object_set_data (G_OBJECT (widget), "DZL_TAB", NULL); + gtk_container_remove (GTK_CONTAINER (self), GTK_WIDGET (tab)); + } +} + +GtkWidget * +dzl_tab_strip_new (void) +{ + return g_object_new (DZL_TYPE_TAB_STRIP, NULL); +} + +/** + * dzl_tab_strip_get_stack: + * + * Returns: (transfer none) (nullable): A #GtkStack or %NULL. + */ +GtkStack * +dzl_tab_strip_get_stack (DzlTabStrip *self) +{ + DzlTabStripPrivate *priv = dzl_tab_strip_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB_STRIP (self), NULL); + + return priv->stack; +} + +static void +dzl_tab_strip_cold_plug (GtkWidget *widget, + gpointer user_data) +{ + DzlTabStrip *self = user_data; + DzlTabStripPrivate *priv = dzl_tab_strip_get_instance_private (self); + + g_assert (DZL_IS_TAB_STRIP (self)); + g_assert (GTK_IS_WIDGET (widget)); + + dzl_tab_strip_stack_add (self, widget, priv->stack); +} + +void +dzl_tab_strip_set_stack (DzlTabStrip *self, + GtkStack *stack) +{ + DzlTabStripPrivate *priv = dzl_tab_strip_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB_STRIP (self)); + g_return_if_fail (!stack || GTK_IS_STACK (stack)); + + if (stack != priv->stack) + { + if (priv->stack != NULL) + { + g_signal_handlers_disconnect_by_func (priv->stack, + G_CALLBACK (dzl_tab_strip_stack_notify_visible_child), + self); + + g_signal_handlers_disconnect_by_func (priv->stack, + G_CALLBACK (dzl_tab_strip_stack_add), + self); + + g_signal_handlers_disconnect_by_func (priv->stack, + G_CALLBACK (dzl_tab_strip_stack_remove), + self); + + gtk_container_foreach (GTK_CONTAINER (self), (GtkCallback)gtk_widget_destroy, NULL); + + g_clear_object (&priv->stack); + } + + if (stack != NULL) + { + priv->stack = g_object_ref (stack); + + g_signal_connect_object (priv->stack, + "notify::visible-child", + G_CALLBACK (dzl_tab_strip_stack_notify_visible_child), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (priv->stack, + "add", + G_CALLBACK (dzl_tab_strip_stack_add), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (priv->stack, + "remove", + G_CALLBACK (dzl_tab_strip_stack_remove), + self, + G_CONNECT_SWAPPED); + + gtk_container_foreach (GTK_CONTAINER (priv->stack), + dzl_tab_strip_cold_plug, + self); + } + } +} + +static void +dzl_tab_strip_update_edge (GtkWidget *widget, + gpointer user_data) +{ + GtkPositionType edge = GPOINTER_TO_INT (user_data); + + g_assert (GTK_IS_WIDGET (widget)); + + if (DZL_IS_TAB (widget)) + dzl_tab_set_edge (DZL_TAB (widget), edge); +} + +GtkPositionType +dzl_tab_strip_get_edge (DzlTabStrip *self) +{ + DzlTabStripPrivate *priv = dzl_tab_strip_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB_STRIP (self), 0); + + return priv->edge; +} + +void +dzl_tab_strip_set_edge (DzlTabStrip *self, + GtkPositionType edge) +{ + DzlTabStripPrivate *priv = dzl_tab_strip_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB_STRIP (self)); + g_return_if_fail (edge >= 0); + g_return_if_fail (edge <= 3); + + if (priv->edge != edge) + { + GtkStyleContext *style_context; + const gchar *class_name = NULL; + + priv->edge = edge; + + gtk_container_foreach (GTK_CONTAINER (self), + dzl_tab_strip_update_edge, + GINT_TO_POINTER (edge)); + + style_context = gtk_widget_get_style_context (GTK_WIDGET (self)); + + gtk_style_context_remove_class (style_context, "left"); + gtk_style_context_remove_class (style_context, "top"); + gtk_style_context_remove_class (style_context, "right"); + gtk_style_context_remove_class (style_context, "bottom"); + + switch (edge) + { + case GTK_POS_LEFT: + class_name = "left"; + break; + + case GTK_POS_RIGHT: + class_name = "right"; + break; + + case GTK_POS_TOP: + class_name = "top"; + break; + + case GTK_POS_BOTTOM: + class_name = "bottom"; + break; + + default: + g_assert_not_reached (); + } + + gtk_style_context_add_class (style_context, class_name); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EDGE]); + } +} + +static void +apply_style (GtkWidget *widget, + gpointer user_data) +{ + DzlTabStyle style = GPOINTER_TO_UINT (user_data); + + g_assert (GTK_IS_WIDGET (widget)); + + if (DZL_IS_TAB (widget)) + dzl_tab_set_style (DZL_TAB (widget), style); +} + +guint +dzl_tab_strip_get_style (DzlTabStrip *self) +{ + DzlTabStripPrivate *priv = dzl_tab_strip_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB_STRIP (self), 0); + + return priv->style; +} + +void +dzl_tab_strip_set_style (DzlTabStrip *self, + DzlTabStyle style) +{ + DzlTabStripPrivate *priv = dzl_tab_strip_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB_STRIP (self)); + + if (style != priv->style) + { + priv->style = style; + gtk_container_foreach (GTK_CONTAINER (self), apply_style, GUINT_TO_POINTER (style)); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STYLE]); + } +} + +void +dzl_tab_strip_add_control (DzlTabStrip *self, + GtkWidget *widget) +{ + g_return_if_fail (DZL_IS_TAB_STRIP (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + + gtk_container_add_with_properties (GTK_CONTAINER (self), widget, + "pack-type", GTK_PACK_END, + "expand", FALSE, + "fill", FALSE, + NULL); + + gtk_style_context_add_class (gtk_widget_get_style_context (widget), "control"); +} + +static void +dzl_tab_strip_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *child_type) +{ + DzlTabStrip *self = (DzlTabStrip *)buildable; + + g_assert (DZL_IS_TAB_STRIP (self)); + g_assert (GTK_IS_BUILDER (builder)); + g_assert (G_IS_OBJECT (child)); + + if (g_strcmp0 (child_type, "control") == 0 && GTK_IS_WIDGET (child)) + dzl_tab_strip_add_control (self, GTK_WIDGET (child)); + else + g_warning ("I do not know how to add %s of type %s", + G_OBJECT_TYPE_NAME (child), child_type ? child_type : "NULL"); +} + +static void +buildable_iface_init (GtkBuildableIface *iface) +{ + iface->add_child = dzl_tab_strip_add_child; +} diff --git a/src/panel/dzl-tab-strip.h b/src/panel/dzl-tab-strip.h new file mode 100644 index 0000000..75cd013 --- /dev/null +++ b/src/panel/dzl-tab-strip.h @@ -0,0 +1,70 @@ +/* dzl-tab-strip.h + * + * Copyright (C) 2016 Christian Hergert + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_TAB_STRIP_H +#define DZL_TAB_STRIP_H + +#include "dzl-version-macros.h" + +#include "dzl-dock-types.h" + +G_BEGIN_DECLS + +struct _DzlTabStripClass +{ + GtkBoxClass parent; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_tab_strip_new (void); +DZL_AVAILABLE_IN_ALL +GtkStack *dzl_tab_strip_get_stack (DzlTabStrip *self); +DZL_AVAILABLE_IN_ALL +void dzl_tab_strip_set_stack (DzlTabStrip *self, + GtkStack *stack); +DZL_AVAILABLE_IN_ALL +GtkPositionType dzl_tab_strip_get_edge (DzlTabStrip *self); +DZL_AVAILABLE_IN_ALL +void dzl_tab_strip_set_edge (DzlTabStrip *self, + GtkPositionType edge); +DZL_AVAILABLE_IN_ALL +DzlTabStyle dzl_tab_strip_get_style (DzlTabStrip *self); +DZL_AVAILABLE_IN_ALL +void dzl_tab_strip_set_style (DzlTabStrip *self, + DzlTabStyle style); +DZL_AVAILABLE_IN_ALL +void dzl_tab_strip_add_control (DzlTabStrip *self, + GtkWidget *widget); + +G_END_DECLS + +#endif /* DZL_TAB_STRIP_H */ diff --git a/src/panel/dzl-tab.c b/src/panel/dzl-tab.c new file mode 100644 index 0000000..c41d3a0 --- /dev/null +++ b/src/panel/dzl-tab.c @@ -0,0 +1,1187 @@ +/* dzl-tab.c + * + * Copyright (C) 2016 Christian Hergert + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define G_LOG_DOMAIN "dzl-tab" + +#include "config.h" + +#include "panel/dzl-dock-item.h" +#include "panel/dzl-tab.h" +#include "panel/dzl-tab-private.h" +#include "util/dzl-util-private.h" + +struct _DzlTab { GtkEventBox parent; }; + +typedef struct +{ + /* + * The edge the tab is being displayed upon. We use this to rotate + * text or alter box orientation. + */ + GtkPositionType edge : 2; + + /* Can we be closed by the user */ + guint can_close : 1; + + /* Are we the active tab */ + guint active : 1; + + /* If we are currently pressed */ + guint pressed : 1; + + /* If the pointer is over the widget (prelight) */ + guint pointer_in_widget : 1; + + /* If we are currently activating */ + guint in_activate : 1; + + /* Our icon/text style */ + DzlTabStyle style : 2; + + /* The action name to change the current tab */ + gchar *action_name; + + /* The value for the current tab */ + GVariant *action_target_value; + + /* + * Because we don't have access to GtkActionMuxer or GtkActionHelper + * from inside of Gtk, we need to manually track the state of the + * action we are monitoring. + * + * We hold a pointer to the group so we can disconnect as necessary. + */ + GActionGroup *action_group; + gulong action_state_changed_handler; + + /* + * These are our control widgets. The box contains the title in the + * center with the minimize/close buttons to a side, depending on the + * orientation/edge of the tabs. + */ + GtkBox *box; + GtkImage *image; + GtkLabel *title; + GtkWidget *close; + GtkWidget *minimize; + + /* + * This is a weak pointer to the widget this tab represents. + * It's used by the stack to translate between the tab/widget + * as necessary. + */ + GtkWidget *widget; +} DzlTabPrivate; + +static void actionable_iface_init (GtkActionableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (DzlTab, dzl_tab, DZL_TYPE_BIN, + G_ADD_PRIVATE (DzlTab) + G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTIONABLE, actionable_iface_init)) + +enum { + PROP_0, + PROP_ACTIVE, + PROP_CAN_CLOSE, + PROP_EDGE, + PROP_STYLE, + PROP_TITLE, + PROP_WIDGET, + N_PROPS, + + PROP_ACTION_NAME, + PROP_ACTION_TARGET, +}; + +enum { + CLICKED, + N_SIGNALS +}; + +static GParamSpec *properties [N_PROPS]; +static guint signals [N_SIGNALS]; + +static void +dzl_tab_get_inner_allocation (DzlTab *self, + GtkAllocation *alloc) +{ + GtkBorder margin; + GtkBorder border; + GtkStyleContext *style_context; + GtkStateFlags flags; + + g_assert (DZL_IS_TAB (self)); + g_assert (alloc != NULL); + + gtk_widget_get_allocation (GTK_WIDGET (self), alloc); + + style_context = gtk_widget_get_style_context (GTK_WIDGET (self)); + flags = gtk_widget_get_state_flags (GTK_WIDGET (self)); + + gtk_style_context_get_border (style_context, flags, &margin); + gtk_style_context_get_border (style_context, flags, &border); + + alloc->x += border.left; + alloc->x += margin.left; + + alloc->y += border.top; + alloc->y += margin.top; + + alloc->width -= (border.right + border.left); + alloc->width -= (margin.right + margin.left); + + alloc->height -= (border.bottom + border.top); + alloc->height -= (margin.bottom + margin.top); +} + +static void +dzl_tab_apply_state (DzlTab *self) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_assert (DZL_IS_TAB (self)); + + if (priv->active) + gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED, FALSE); + else + gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED); +} + +static void +dzl_tab_activate (DzlTab *self) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + g_autoptr(GVariant) value = NULL; + + g_assert (DZL_IS_TAB (self)); + + if (priv->in_activate || + priv->action_name == NULL || + priv->action_target_value == NULL) + return; + + priv->in_activate = TRUE; + + value = dzl_gtk_widget_get_action_state (GTK_WIDGET (self), priv->action_name); + if (value != NULL && !g_variant_equal (value, priv->action_target_value)) + dzl_gtk_widget_activate_action (GTK_WIDGET (self), priv->action_name, priv->action_target_value); + + priv->in_activate = FALSE; +} + +static void +dzl_tab_destroy (GtkWidget *widget) +{ + DzlTab *self = (DzlTab *)widget; + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + if (priv->action_group != NULL) + { + g_signal_handler_disconnect (priv->action_group, + priv->action_state_changed_handler); + priv->action_state_changed_handler = 0; + dzl_clear_weak_pointer (&priv->action_group); + } + + dzl_clear_weak_pointer (&priv->widget); + g_clear_pointer (&priv->action_name, g_free); + g_clear_pointer (&priv->action_target_value, g_variant_unref); + + GTK_WIDGET_CLASS (dzl_tab_parent_class)->destroy (widget); +} + +static void +dzl_tab_update_edge (DzlTab *self) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_assert (DZL_IS_TAB (self)); + + switch (priv->edge) + { + case GTK_POS_TOP: + gtk_label_set_angle (priv->title, 0.0); + gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->box), GTK_ORIENTATION_HORIZONTAL); + gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->close), FALSE, FALSE, 0, GTK_PACK_END); + gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->minimize), FALSE, FALSE, 0, GTK_PACK_END); + gtk_widget_set_hexpand (GTK_WIDGET (self), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self), FALSE); + break; + + case GTK_POS_BOTTOM: + gtk_label_set_angle (priv->title, 0.0); + gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->box), GTK_ORIENTATION_HORIZONTAL); + gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->close), FALSE, FALSE, 0, GTK_PACK_END); + gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->minimize), FALSE, FALSE, 0, GTK_PACK_END); + gtk_widget_set_hexpand (GTK_WIDGET (self), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self), FALSE); + break; + + case GTK_POS_LEFT: + gtk_label_set_angle (priv->title, -90.0); + gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->box), GTK_ORIENTATION_VERTICAL); + gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->close), FALSE, FALSE, 0, GTK_PACK_END); + gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->minimize), FALSE, FALSE, 0, GTK_PACK_END); + gtk_widget_set_hexpand (GTK_WIDGET (self), FALSE); + gtk_widget_set_vexpand (GTK_WIDGET (self), TRUE); + break; + + case GTK_POS_RIGHT: + gtk_label_set_angle (priv->title, 90.0); + gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->box), GTK_ORIENTATION_VERTICAL); + gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->close), FALSE, FALSE, 0, GTK_PACK_START); + gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->minimize), FALSE, FALSE, 0, GTK_PACK_START); + gtk_widget_set_hexpand (GTK_WIDGET (self), FALSE); + gtk_widget_set_vexpand (GTK_WIDGET (self), TRUE); + break; + + default: + g_assert_not_reached (); + } +} + +static gboolean +get_widget_coordinates (GtkWidget *widget, + GdkEvent *event, + gdouble *x, + gdouble *y) +{ + GdkWindow *window = ((GdkEventAny *)event)->window; + GdkWindow *target = gtk_widget_get_window (widget); + gdouble tx, ty; + + if (!gdk_event_get_coords (event, &tx, &ty)) + return FALSE; + + while (window && window != target) + { + gint window_x, window_y; + + gdk_window_get_position (window, &window_x, &window_y); + tx += window_x; + ty += window_y; + + window = gdk_window_get_parent (window); + } + + if (window) + { + *x = tx; + *y = ty; + + return TRUE; + } + else + return FALSE; +} + +static void +dzl_tab_update_prelight (DzlTab *self, + GdkEvent *event) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + gdouble x, y; + + g_assert (DZL_IS_TAB (self)); + g_assert (event != NULL); + + if (get_widget_coordinates (GTK_WIDGET (self), (GdkEvent *)event, &x, &y)) + { + GtkAllocation alloc; + + dzl_tab_get_inner_allocation (self, &alloc); + + /* + * We've translated our x,y coords to be relative to the widget, so we + * can discard the x,y of the allocation. + */ + alloc.x = 0; + alloc.y = 0; + + if (x >= alloc.x && + x <= (alloc.x + alloc.width) && + y >= alloc.y && + y <= (alloc.y + alloc.height)) + { + priv->pointer_in_widget = TRUE; + gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_PRELIGHT, FALSE); + return; + } + } + + priv->pointer_in_widget = FALSE; + gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_PRELIGHT); +} + +static gboolean +dzl_tab_motion_notify_event (GtkWidget *widget, + GdkEventMotion *event) +{ + DzlTab *self = (DzlTab *)widget; + + g_assert (DZL_IS_TAB (self)); + g_assert (event != NULL); + + dzl_tab_update_prelight (self, (GdkEvent *)event); + + return GDK_EVENT_PROPAGATE; +} + +static gboolean +dzl_tab_enter_notify_event (GtkWidget *widget, + GdkEventCrossing *event) +{ + DzlTab *self = (DzlTab *)widget; + + g_assert (DZL_IS_TAB (self)); + g_assert (event != NULL); + + dzl_tab_update_prelight (self, (GdkEvent *)event); + + return GDK_EVENT_PROPAGATE; +} + +static gboolean +dzl_tab_leave_notify_event (GtkWidget *widget, + GdkEventCrossing *event) +{ + DzlTab *self = (DzlTab *)widget; + + g_return_val_if_fail (DZL_IS_TAB (self), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + dzl_tab_update_prelight (self, (GdkEvent *)event); + + return GDK_EVENT_PROPAGATE; +} + +static gboolean +dzl_tab_button_press_event (GtkWidget *widget, + GdkEventButton *event) +{ + DzlTab *self = (DzlTab *)widget; + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB (self), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + if (event->button == GDK_BUTTON_PRIMARY) + { + priv->pressed = TRUE; + gtk_widget_set_state_flags (widget, GTK_STATE_FLAG_ACTIVE, FALSE); + gtk_widget_grab_focus (GTK_WIDGET (self)); + return GDK_EVENT_STOP; + } + + return GDK_EVENT_PROPAGATE; +} + +static gboolean +dzl_tab_button_release_event (GtkWidget *widget, + GdkEventButton *event) +{ + DzlTab *self = (DzlTab *)widget; + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB (self), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + if (event->button == GDK_BUTTON_PRIMARY) + { + priv->pressed = FALSE; + gtk_widget_unset_state_flags (widget, GTK_STATE_FLAG_ACTIVE); + + if (priv->pointer_in_widget) + g_signal_emit (self, signals [CLICKED], 0); + + return GDK_EVENT_STOP; + } + + return GDK_EVENT_PROPAGATE; +} + +static void +dzl_tab_realize (GtkWidget *widget) +{ + DzlTab *self = (DzlTab *)widget; + GdkWindowAttr attributes = { 0 }; + GdkWindow *parent; + GdkWindow *window; + GtkAllocation alloc; + gint attributes_mask = 0; + + g_assert (DZL_IS_TAB (widget)); + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + gtk_widget_set_realized (GTK_WIDGET (self), TRUE); + + parent = gtk_widget_get_parent_window (GTK_WIDGET (self)); + + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.x = alloc.x; + attributes.y = alloc.y; + attributes.width = alloc.width; + attributes.height = alloc.height; + attributes.visual = gtk_widget_get_visual (widget); + attributes.event_mask = gtk_widget_get_events (widget); + attributes.event_mask |= (GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_TOUCH_MASK | + GDK_EXPOSURE_MASK | + GDK_ENTER_NOTIFY_MASK | + GDK_LEAVE_NOTIFY_MASK); + + attributes_mask = (GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL); + + window = gdk_window_new (parent, &attributes, attributes_mask); + gtk_widget_register_window (GTK_WIDGET (self), window); + gtk_widget_set_window (GTK_WIDGET (self), window); +} + +static void +dzl_tab_action_state_changed (DzlTab *self, + const gchar *action_name, + GVariant *value, + GActionGroup *group) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + gboolean active; + + g_assert (DZL_IS_TAB (self)); + g_assert (action_name != NULL); + g_assert (G_IS_ACTION_GROUP (group)); + + active = (value != NULL && + priv->action_target_value != NULL && + g_variant_equal (value, priv->action_target_value)); + + if (active != priv->active) + { + priv->active = active; + dzl_tab_apply_state (self); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ACTIVE]); + } +} + +static void +dzl_tab_monitor_action_group (DzlTab *self, + GActionGroup *group) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB (self)); + g_return_if_fail (!group || G_IS_ACTION_GROUP (group)); + + if (group != priv->action_group) + { + if (priv->action_group != NULL) + { + g_signal_handler_disconnect (priv->action_group, + priv->action_state_changed_handler); + priv->action_state_changed_handler = 0; + dzl_clear_weak_pointer (&priv->action_group); + } + + if (group != NULL) + { + gchar *prefix = NULL; + gchar *name = NULL; + + dzl_g_action_name_parse (priv->action_name, &prefix, &name); + + if (name != NULL) + { + gchar *detailed; + + detailed = g_strdup_printf ("action-state-changed::%s", name); + dzl_set_weak_pointer (&priv->action_group, group); + priv->action_state_changed_handler = + g_signal_connect_object (priv->action_group, + detailed, + G_CALLBACK (dzl_tab_action_state_changed), + self, + G_CONNECT_SWAPPED); + + g_free (detailed); + } + + g_free (prefix); + g_free (name); + } + } +} + +static void +dzl_tab_hierarchy_changed (GtkWidget *widget, + GtkWidget *old_toplevel) +{ + DzlTab *self = (DzlTab *)widget; + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + GActionGroup *group; + + g_assert (GTK_IS_WIDGET (widget)); + g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel)); + + group = dzl_gtk_widget_find_group_for_action (widget, priv->action_name); + dzl_tab_monitor_action_group (self, group); +} + +static void +dzl_tab_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + g_assert (DZL_IS_TAB (widget)); + + GTK_WIDGET_CLASS (dzl_tab_parent_class)->size_allocate (widget, allocation); + + if (gtk_widget_get_realized (widget)) + gdk_window_move_resize (gtk_widget_get_window (widget), + allocation->x, + allocation->y, + allocation->width, + allocation->height); +} + +static void +dzl_tab_close_clicked (DzlTab *self, + GtkWidget *button) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_assert (DZL_IS_TAB (self)); + g_assert (GTK_IS_BUTTON (button)); + + g_object_ref (self); + + if (DZL_IS_DOCK_ITEM (priv->widget) && + dzl_dock_item_get_can_close (DZL_DOCK_ITEM (priv->widget))) + dzl_dock_item_close (DZL_DOCK_ITEM (priv->widget)); + + g_object_unref (self); +} + +static void +dzl_tab_minimize_clicked (DzlTab *self, + GtkWidget *button) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + GtkPositionType position = GTK_POS_LEFT; + + g_assert (DZL_IS_TAB (self)); + g_assert (GTK_IS_BUTTON (button)); + + g_object_ref (self); + + if (DZL_IS_DOCK_ITEM (priv->widget)) + { + DzlDockItem *item = DZL_DOCK_ITEM (priv->widget); + + for (DzlDockItem *parent = dzl_dock_item_get_parent (item); + parent != NULL; + parent = dzl_dock_item_get_parent (parent)) + { + if (dzl_dock_item_minimize (parent, item, &position)) + break; + } + } + + g_object_unref (self); +} + +static gboolean +dzl_tab_query_tooltip (GtkWidget *widget, + gint x, + gint y, + gboolean keyboard, + GtkTooltip *tooltip) +{ + DzlTab *self = (DzlTab *)widget; + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + const gchar *title; + + g_assert (DZL_IS_TAB (self)); + g_assert (GTK_IS_TOOLTIP (tooltip)); + + title = gtk_label_get_label (priv->title); + + if (title != NULL) + { + gtk_tooltip_set_text (tooltip, title); + return TRUE; + } + + return FALSE; +} + +static void +dzl_tab_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlTab *self = DZL_TAB (object); + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + switch (prop_id) + { + case PROP_ACTIVE: + g_value_set_boolean (value, dzl_tab_get_active (self)); + break; + + case PROP_ACTION_NAME: + g_value_set_string (value, priv->action_name); + break; + + case PROP_ACTION_TARGET: + g_value_set_boxed (value, priv->action_target_value); + break; + + case PROP_CAN_CLOSE: + g_value_set_boolean (value, dzl_tab_get_can_close (self)); + break; + + case PROP_EDGE: + g_value_set_enum (value, dzl_tab_get_edge (self)); + break; + + case PROP_STYLE: + g_value_set_flags (value, dzl_tab_get_style (self)); + break; + + case PROP_TITLE: + g_value_set_string (value, dzl_tab_get_title (self)); + break; + + case PROP_WIDGET: + g_value_set_object (value, dzl_tab_get_widget (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_tab_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlTab *self = DZL_TAB (object); + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + switch (prop_id) + { + case PROP_ACTIVE: + dzl_tab_set_active (self, g_value_get_boolean (value)); + break; + + case PROP_ACTION_NAME: + g_free (priv->action_name); + priv->action_name = g_value_dup_string (value); + break; + + case PROP_ACTION_TARGET: + g_clear_pointer (&priv->action_target_value, g_variant_unref); + priv->action_target_value = g_value_get_variant (value); + if (priv->action_target_value != NULL) + g_variant_ref_sink (priv->action_target_value); + break; + + case PROP_CAN_CLOSE: + dzl_tab_set_can_close (self, g_value_get_boolean (value)); + break; + + case PROP_EDGE: + dzl_tab_set_edge (self, g_value_get_enum (value)); + break; + + case PROP_STYLE: + dzl_tab_set_style (self, g_value_get_flags (value)); + break; + + case PROP_TITLE: + dzl_tab_set_title (self, g_value_get_string (value)); + break; + + case PROP_WIDGET: + dzl_tab_set_widget (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_tab_class_init (DzlTabClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = dzl_tab_get_property; + object_class->set_property = dzl_tab_set_property; + + widget_class->destroy = dzl_tab_destroy; + widget_class->button_press_event = dzl_tab_button_press_event; + widget_class->button_release_event = dzl_tab_button_release_event; + widget_class->motion_notify_event = dzl_tab_motion_notify_event; + widget_class->enter_notify_event = dzl_tab_enter_notify_event; + widget_class->leave_notify_event = dzl_tab_leave_notify_event; + widget_class->realize = dzl_tab_realize; + widget_class->size_allocate = dzl_tab_size_allocate; + widget_class->hierarchy_changed = dzl_tab_hierarchy_changed; + widget_class->query_tooltip = dzl_tab_query_tooltip; + + gtk_widget_class_set_css_name (widget_class, "dzltab"); + + g_object_class_override_property (object_class, PROP_ACTION_NAME, "action-name"); + g_object_class_override_property (object_class, PROP_ACTION_TARGET, "action-target"); + + properties [PROP_ACTIVE] = + g_param_spec_boolean ("active", + "Active", + "If the tab is currently active", + FALSE, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_CAN_CLOSE] = + g_param_spec_boolean ("can-close", + "Can Close", + "If the tab widget can be closed", + FALSE, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_EDGE] = + g_param_spec_enum ("edge", + "Edge", + "Edge", + GTK_TYPE_POSITION_TYPE, + GTK_POS_TOP, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_STYLE] = + g_param_spec_flags ("style", + "Style", + "The style for the tab", + DZL_TYPE_TAB_STYLE, + DZL_TAB_BOTH, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "Title", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_WIDGET] = + g_param_spec_object ("widget", + "Widget", + "The widget the tab represents", + GTK_TYPE_WIDGET, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + signals [CLICKED] = + g_signal_new_class_handler ("clicked", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_CALLBACK (dzl_tab_activate), + NULL, NULL, NULL, G_TYPE_NONE, 0); +} + +static void +dzl_tab_init (DzlTab *self) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + GtkBox *center; + + priv->style = DZL_TAB_BOTH; + priv->edge = GTK_POS_TOP; + + gtk_widget_set_has_window (GTK_WIDGET (self), TRUE); + + gtk_widget_add_events (GTK_WIDGET (self), + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + GDK_ENTER_NOTIFY_MASK| + GDK_LEAVE_NOTIFY_MASK); + gtk_widget_set_hexpand (GTK_WIDGET (self), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self), FALSE); + + gtk_widget_set_has_tooltip (GTK_WIDGET (self), TRUE); + + priv->box = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "visible", TRUE, + NULL); + g_signal_connect (priv->box, "destroy", G_CALLBACK (gtk_widget_destroyed), &priv->box); + gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (priv->box)); + + center = g_object_new (GTK_TYPE_BOX, + "spacing", 6, + "visible", TRUE, + NULL); + gtk_box_set_center_widget (priv->box, GTK_WIDGET (center)); + + priv->image = g_object_new (GTK_TYPE_IMAGE, + "visible", TRUE, + NULL); + g_signal_connect (priv->image, "destroy", G_CALLBACK (gtk_widget_destroyed), &priv->image); + gtk_box_pack_start (center, GTK_WIDGET (priv->image), FALSE, FALSE, 0); + + priv->title = g_object_new (GTK_TYPE_LABEL, + "ellipsize", PANGO_ELLIPSIZE_END, + "use-underline", TRUE, + "visible", TRUE, + NULL); + g_signal_connect (priv->title, "destroy", G_CALLBACK (gtk_widget_destroyed), &priv->title); + gtk_box_pack_start (center, GTK_WIDGET (priv->title), FALSE, FALSE, 0); + + priv->close = g_object_new (GTK_TYPE_BUTTON, + "halign", GTK_ALIGN_END, + "child", g_object_new (GTK_TYPE_IMAGE, + "icon-name", "window-close-symbolic", + "visible", TRUE, + NULL), + "visible", TRUE, + NULL); + g_signal_connect_object (priv->close, + "clicked", + G_CALLBACK (dzl_tab_close_clicked), + self, + G_CONNECT_SWAPPED); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->close)), "close"); + gtk_box_pack_end (priv->box, GTK_WIDGET (priv->close), FALSE, FALSE, 0); + g_object_bind_property (self, "can-close", priv->close, "visible", G_BINDING_SYNC_CREATE); + + priv->minimize = g_object_new (GTK_TYPE_BUTTON, + "halign", GTK_ALIGN_END, + "child", g_object_new (GTK_TYPE_IMAGE, + "icon-name", "window-minimize-symbolic", + "visible", TRUE, + NULL), + "visible", TRUE, + NULL); + g_signal_connect_object (priv->minimize, + "clicked", + G_CALLBACK (dzl_tab_minimize_clicked), + self, + G_CONNECT_SWAPPED); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->minimize)), "minimize"); + gtk_box_pack_end (priv->box, GTK_WIDGET (priv->minimize), FALSE, FALSE, 0); +} + +const gchar * +dzl_tab_get_title (DzlTab *self) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB (self), NULL); + + return gtk_label_get_label (priv->title); +} + +void +dzl_tab_set_title (DzlTab *self, + const gchar *title) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB (self)); + + gtk_label_set_label (priv->title, title); +} + +const gchar * +dzl_tab_get_icon_name (DzlTab *self) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + const gchar *ret = NULL; + + g_return_val_if_fail (DZL_IS_TAB (self), NULL); + + gtk_image_get_icon_name (priv->image, &ret, NULL); + + return ret; +} + +void +dzl_tab_set_icon_name (DzlTab *self, + const gchar *icon_name) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB (self)); + + g_object_set (priv->image, "icon-name", icon_name, NULL); +} + +GtkPositionType +dzl_tab_get_edge (DzlTab *self) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB (self), 0); + + return priv->edge; +} + +void +dzl_tab_set_edge (DzlTab *self, + GtkPositionType edge) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB (self)); + g_return_if_fail (edge >= 0); + g_return_if_fail (edge <= 3); + + if (priv->edge != edge) + { + priv->edge = edge; + dzl_tab_update_edge (self); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EDGE]); + } +} + +/** + * dzl_tab_get_widget: + * + * Returns: (transfer none) (nullable): A #GtkWidget or %NULL. + */ +GtkWidget * +dzl_tab_get_widget (DzlTab *self) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB (self), NULL); + + return priv->widget; +} + +void +dzl_tab_set_widget (DzlTab *self, + GtkWidget *widget) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB (self)); + + if (dzl_set_weak_pointer (&priv->widget, widget)) + { + gtk_label_set_mnemonic_widget (priv->title, widget); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WIDGET]); + } +} + +gboolean +dzl_tab_get_can_close (DzlTab *self) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB (self), FALSE); + + return priv->can_close; +} + +void +dzl_tab_set_can_close (DzlTab *self, + gboolean can_close) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB (self)); + + can_close = !!can_close; + + if (can_close != priv->can_close) + { + priv->can_close = can_close; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CAN_CLOSE]); + } +} + +gboolean +dzl_tab_get_active (DzlTab *self) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB (self), FALSE); + + return priv->active; +} + +void +dzl_tab_set_active (DzlTab *self, + gboolean active) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB (self)); + + active = !!active; + + if (priv->active != active) + { + priv->active = active; + dzl_tab_activate (self); + dzl_tab_apply_state (self); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVE]); + } +} + +static const gchar * +dzl_tab_get_action_name (GtkActionable *actionable) +{ + DzlTab *self = (DzlTab *)actionable; + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB (self), NULL); + + return priv->action_name; +} + +static void +dzl_tab_set_action_name (GtkActionable *actionable, + const gchar *action_name) +{ + DzlTab *self = (DzlTab *)actionable; + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB (self)); + + if (g_strcmp0 (priv->action_name, action_name) != 0) + { + g_free (priv->action_name); + priv->action_name = g_strdup (action_name); + g_object_notify (G_OBJECT (self), "action-name"); + } +} + +static GVariant * +dzl_tab_get_action_target_value (GtkActionable *actionable) +{ + DzlTab *self = (DzlTab *)actionable; + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB (self), NULL); + + return priv->action_target_value; +} + +static void +dzl_tab_set_action_target_value (GtkActionable *actionable, + GVariant *variant) +{ + DzlTab *self = (DzlTab *)actionable; + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB (self)); + + if (priv->action_target_value != variant) + { + g_clear_pointer (&priv->action_target_value, g_variant_unref); + if (variant != NULL) + priv->action_target_value = g_variant_ref_sink (variant); + g_object_notify (G_OBJECT (self), "action-target"); + } +} + +static void +actionable_iface_init (GtkActionableInterface *iface) +{ + iface->get_action_name = dzl_tab_get_action_name; + iface->set_action_name = dzl_tab_set_action_name; + iface->get_action_target_value = dzl_tab_get_action_target_value; + iface->set_action_target_value = dzl_tab_set_action_target_value; +} + +DzlTabStyle +dzl_tab_get_style (DzlTab *self) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TAB (self), 0); + + return priv->style; +} + +void +dzl_tab_set_style (DzlTab *self, + DzlTabStyle style) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + + g_return_if_fail (DZL_IS_TAB (self)); + + if (style != priv->style) + { + priv->style = style; + gtk_widget_set_visible (GTK_WIDGET (priv->image), !!(priv->style & DZL_TAB_ICONS)); + gtk_widget_set_visible (GTK_WIDGET (priv->title), !!(priv->style & DZL_TAB_TEXT)); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STYLE]); + } +} + +void +_dzl_tab_update_controls (DzlTab *self) +{ + DzlTabPrivate *priv = dzl_tab_get_instance_private (self); + gboolean can_close = FALSE; + gboolean can_minimize = FALSE; + + g_return_if_fail (DZL_IS_TAB (self)); + + if (DZL_IS_DOCK_ITEM (priv->widget)) + { + can_close = dzl_dock_item_get_can_close (DZL_DOCK_ITEM (priv->widget)); + can_minimize = dzl_dock_item_get_can_minimize (DZL_DOCK_ITEM (priv->widget)); + } + + gtk_widget_set_visible (GTK_WIDGET (priv->close), can_close); + gtk_widget_set_visible (GTK_WIDGET (priv->minimize), can_minimize); +} + +GType +dzl_tab_style_get_type (void) +{ + static GType type_id; + + if (g_once_init_enter (&type_id)) + { + GType _type_id; + static const GFlagsValue values[] = { + { DZL_TAB_ICONS, "DZL_TAB_ICONS", "icons" }, + { DZL_TAB_TEXT, "DZL_TAB_TEXT", "text" }, + { DZL_TAB_BOTH, "DZL_TAB_BOTH", "both" }, + { 0 } + }; + + _type_id = g_flags_register_static ("DzlTabStyle", values); + g_once_init_leave (&type_id, _type_id); + } + + return type_id; +} diff --git a/src/panel/dzl-tab.h b/src/panel/dzl-tab.h new file mode 100644 index 0000000..dcd4209 --- /dev/null +++ b/src/panel/dzl-tab.h @@ -0,0 +1,71 @@ +/* dzl-tab.h + * + * Copyright (C) 2016 Christian Hergert + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_TAB_H +#define DZL_TAB_H + +#include "dzl-version-macros.h" + +#include "dzl-dock-types.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +const gchar *dzl_tab_get_icon_name (DzlTab *self); +DZL_AVAILABLE_IN_ALL +void dzl_tab_set_icon_name (DzlTab *self, + const gchar *icon_name); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_tab_get_title (DzlTab *self); +DZL_AVAILABLE_IN_ALL +void dzl_tab_set_title (DzlTab *self, + const gchar *title); +DZL_AVAILABLE_IN_ALL +GtkPositionType dzl_tab_get_edge (DzlTab *self); +DZL_AVAILABLE_IN_ALL +void dzl_tab_set_edge (DzlTab *self, + GtkPositionType edge); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_tab_get_widget (DzlTab *self); +DZL_AVAILABLE_IN_ALL +void dzl_tab_set_widget (DzlTab *self, + GtkWidget *widget); +DZL_AVAILABLE_IN_ALL +gboolean dzl_tab_get_active (DzlTab *self); +DZL_AVAILABLE_IN_ALL +void dzl_tab_set_active (DzlTab *self, + gboolean active); +DZL_AVAILABLE_IN_ALL +gboolean dzl_tab_get_can_close (DzlTab *self); +DZL_AVAILABLE_IN_ALL +void dzl_tab_set_can_close (DzlTab *self, + gboolean can_close); +DZL_AVAILABLE_IN_ALL +DzlTabStyle dzl_tab_get_style (DzlTab *self); +DZL_AVAILABLE_IN_ALL +void dzl_tab_set_style (DzlTab *self, + DzlTabStyle style); + +G_END_DECLS + +#endif /* DZL_TAB_H */ diff --git a/src/panel/meson.build b/src/panel/meson.build new file mode 100644 index 0000000..2b28813 --- /dev/null +++ b/src/panel/meson.build @@ -0,0 +1,41 @@ +panel_headers = [ + 'dzl-dock-bin-edge.h', + 'dzl-dock-bin.h', + 'dzl-dock-item.h', + 'dzl-dock-manager.h', + 'dzl-dock-overlay-edge.h', + 'dzl-dock-overlay.h', + 'dzl-dock-paned.h', + 'dzl-dock-revealer.h', + 'dzl-dock-stack.h', + 'dzl-dock-transient-grab.h', + 'dzl-dock-types.h', + 'dzl-dock-widget.h', + 'dzl-dock-window.h', + 'dzl-dock.h', + 'dzl-tab-strip.h', + 'dzl-tab.h', +] + +panel_sources = [ + 'dzl-dock-bin-edge.c', + 'dzl-dock-bin.c', + 'dzl-dock-item.c', + 'dzl-dock-manager.c', + 'dzl-dock-overlay-edge.c', + 'dzl-dock-overlay.c', + 'dzl-dock-paned.c', + 'dzl-dock-revealer.c', + 'dzl-dock-stack.c', + 'dzl-dock-transient-grab.c', + 'dzl-dock-widget.c', + 'dzl-dock-window.c', + 'dzl-dock.c', + 'dzl-tab-strip.c', + 'dzl-tab.c', +] + +libdazzle_public_headers += files(panel_headers) +libdazzle_public_sources += files(panel_sources) + +install_headers(panel_headers, subdir: join_paths(libdazzle_header_subdir, 'panel')) diff --git a/src/panel/panel-bottom-pane-symbolic.svg b/src/panel/panel-bottom-pane-symbolic.svg new file mode 100644 index 0000000..a13bb63 --- /dev/null +++ b/src/panel/panel-bottom-pane-symbolic.svg @@ -0,0 +1,26 @@ + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + + + + Gnome Symbolic Icon Theme + + + + + + + + + diff --git a/src/panel/panel-left-pane-symbolic.svg b/src/panel/panel-left-pane-symbolic.svg new file mode 100644 index 0000000..116173d --- /dev/null +++ b/src/panel/panel-left-pane-symbolic.svg @@ -0,0 +1,26 @@ + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + + + + Gnome Symbolic Icon Theme + + + + + + + + + diff --git a/src/panel/panel-right-pane-symbolic.svg b/src/panel/panel-right-pane-symbolic.svg new file mode 100644 index 0000000..1c099a7 --- /dev/null +++ b/src/panel/panel-right-pane-symbolic.svg @@ -0,0 +1,26 @@ + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + + + + Gnome Symbolic Icon Theme + + + + + + + + + diff --git a/src/pathbar/dzl-path-bar.c b/src/pathbar/dzl-path-bar.c new file mode 100644 index 0000000..a5a7e96 --- /dev/null +++ b/src/pathbar/dzl-path-bar.c @@ -0,0 +1,453 @@ +/* dzl-path-bar.c + * + * Copyright (C) 2016-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-path-bar" + +#include "config.h" + +#include +#include + +#include "dzl-path.h" +#include "dzl-path-bar.h" +#include "dzl-path-element.h" + +struct _DzlPathBar +{ + GtkBox parent_instance; + DzlPath *path; +}; + +G_DEFINE_TYPE (DzlPathBar, dzl_path_bar, GTK_TYPE_BOX) + +enum { + PROP_0, + PROP_PATH, + N_PROPS +}; + +enum { + ELEMENT_SELECTED, + POPULATE_MENU, + N_SIGNALS +}; + +static GParamSpec *properties [N_PROPS]; +static guint signals [N_SIGNALS]; + +static void dzl_path_bar_button_clicked (DzlPathBar *self, + GtkToggleButton *button); + +GtkWidget * +dzl_path_bar_new (void) +{ + return g_object_new (DZL_TYPE_PATH_BAR, NULL); +} + +static void +dzl_path_bar_finalize (GObject *object) +{ + DzlPathBar *self = (DzlPathBar *)object; + + g_clear_object (&self->path); + + G_OBJECT_CLASS (dzl_path_bar_parent_class)->finalize (object); +} + +static void +dzl_path_bar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPathBar *self = DZL_PATH_BAR (object); + + switch (prop_id) + { + case PROP_PATH: + g_value_set_object (value, self->path); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_path_bar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPathBar *self = DZL_PATH_BAR (object); + + switch (prop_id) + { + case PROP_PATH: + dzl_path_bar_set_path (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_path_bar_class_init (DzlPathBarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_path_bar_finalize; + object_class->get_property = dzl_path_bar_get_property; + object_class->set_property = dzl_path_bar_set_property; + + properties [PROP_PATH] = + g_param_spec_object ("path", + "Path", + "Path", + DZL_TYPE_PATH, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + signals [ELEMENT_SELECTED] = + g_signal_new ("element-selected", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 2, DZL_TYPE_PATH, DZL_TYPE_PATH_ELEMENT); + + signals [POPULATE_MENU] = + g_signal_new ("populate-menu", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 3, DZL_TYPE_PATH, DZL_TYPE_PATH_ELEMENT, G_TYPE_MENU); + + gtk_widget_class_set_css_name (widget_class, "dzlpathbar"); +} + +static void +dzl_path_bar_init (DzlPathBar *self) +{ + gtk_box_set_spacing (GTK_BOX (self), 12); +} + +static void +dzl_path_bar_buttons_foreach_cb (GtkWidget *widget, + gpointer user_data) +{ + struct { + GtkCallback callback; + gpointer user_data; + } *closure = user_data; + + g_assert (GTK_IS_WIDGET (widget)); + g_assert (closure != NULL); + g_assert (closure->callback != NULL); + + if (GTK_IS_BOX (widget)) + gtk_container_foreach (GTK_CONTAINER (widget), + dzl_path_bar_buttons_foreach_cb, + closure); + else if (GTK_IS_TOGGLE_BUTTON (widget)) + closure->callback (widget, closure->user_data); +} + +static void +dzl_path_bar_buttons_foreach (DzlPathBar *self, + GtkCallback callback, + gpointer user_data) +{ + struct { + GtkCallback callback; + gpointer user_data; + } closure = { callback, user_data }; + + g_assert (DZL_IS_PATH_BAR (self)); + g_assert (callback != NULL); + + gtk_container_foreach (GTK_CONTAINER (self), + dzl_path_bar_buttons_foreach_cb, + &closure); +} + +static void +dzl_path_bar_set_blocked_cb (GtkWidget *widget, + gpointer user_data) +{ + struct { + DzlPathBar *self; + gboolean block; + } *block_data = user_data; + + g_assert (GTK_IS_WIDGET (widget)); + g_assert (block_data != NULL); + g_assert (DZL_IS_PATH_BAR (block_data->self)); + + if (block_data->block) + g_signal_handlers_block_by_func (widget, + dzl_path_bar_button_clicked, + block_data->self); + else + g_signal_handlers_unblock_by_func (widget, + dzl_path_bar_button_clicked, + block_data->self); +} + +static void +dzl_path_bar_set_blocked (DzlPathBar *self, + gboolean blocked) +{ + struct { + DzlPathBar *self; + gboolean block; + } block_data = { self, blocked }; + + g_assert (DZL_IS_PATH_BAR (self)); + + dzl_path_bar_buttons_foreach (self, + dzl_path_bar_set_blocked_cb, + &block_data); +} + +static void +dzl_path_bar_set_active_cb (GtkWidget *widget, + gpointer user_data) +{ + GtkWidget *active = user_data; + + g_assert (GTK_IS_TOGGLE_BUTTON (widget)); + g_assert (GTK_IS_TOGGLE_BUTTON (active)); + + if (active != widget) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), FALSE); +} + +static void +dzl_path_bar_button_clicked (DzlPathBar *self, + GtkToggleButton *button) +{ + g_autoptr(GMenu) menu = NULL; + DzlPathElement *element; + + g_assert (DZL_IS_PATH_BAR (self)); + g_assert (GTK_IS_TOGGLE_BUTTON (button)); + + /* block all other clicked events while we process */ + dzl_path_bar_set_blocked (self, TRUE); + + /* don't allow unselecting the item */ + if (!gtk_toggle_button_get_active (button)) + gtk_toggle_button_set_active (button, TRUE); + + /* set all the other items as disabled */ + dzl_path_bar_buttons_foreach (self, + dzl_path_bar_set_active_cb, + button); + + /* possibly show a menu for this item */ + menu = g_menu_new (); + element = g_object_get_data (G_OBJECT (button), "DZL_PATH_ELEMENT"); + g_assert (DZL_IS_PATH_ELEMENT (element)); + g_signal_emit (self, signals [POPULATE_MENU], 0, self->path, element, menu); + if (g_menu_model_get_n_items (G_MENU_MODEL (menu)) != 0) + { + GtkPopover *popover; + + popover = g_object_new (GTK_TYPE_POPOVER, + "modal", TRUE, + "relative-to", button, + "position", GTK_POS_BOTTOM, + NULL); + gtk_popover_bind_model (popover, G_MENU_MODEL (menu), NULL); + g_signal_connect (popover, + "closed", + G_CALLBACK (gtk_widget_destroy), + NULL); + gtk_widget_show (GTK_WIDGET (popover)); + } + + /* unblock clicked events again */ + dzl_path_bar_set_blocked (self, FALSE); + + /* notify handlers that an item was selected */ + g_signal_emit (self, signals [ELEMENT_SELECTED], 0, self->path, element); +} + +static GtkWidget * +dzl_path_bar_create_element (DzlPathBar *self, + DzlPathElement *element, + gboolean is_last) +{ + const gchar *title; + const gchar *icon_name; + GtkToggleButton *button; + GtkLabel *slash; + GtkLabel *label; + GtkImage *image; + GtkBox *box; + GtkBox *box2; + + g_assert (DZL_IS_PATH_BAR (self)); + g_assert (DZL_IS_PATH_ELEMENT (element)); + + title = dzl_path_element_get_title (element); + icon_name = dzl_path_element_get_icon_name (element); + + box = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "spacing", 12, + "visible", TRUE, + "valign", GTK_ALIGN_BASELINE, + NULL); + + button = g_object_new (GTK_TYPE_TOGGLE_BUTTON, + "active", is_last == TRUE, + "focus-on-click", FALSE, + "visible", TRUE, + "valign", GTK_ALIGN_BASELINE, + NULL); + g_object_set_data_full (G_OBJECT (button), + "DZL_PATH_ELEMENT", + g_object_ref (element), + g_object_unref); + g_signal_connect_object (button, + "clicked", + G_CALLBACK (dzl_path_bar_button_clicked), + self, + G_CONNECT_SWAPPED | G_CONNECT_AFTER); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (button)); + + slash = g_object_new (GTK_TYPE_LABEL, + "label", "/", + "valign", GTK_ALIGN_BASELINE, + "visible", is_last == FALSE, + NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (slash)), "separator"); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (slash)); + + box2 = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "spacing", 6, + "valign", GTK_ALIGN_BASELINE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (button), GTK_WIDGET (box2)); + + if (icon_name != NULL) + { + image = g_object_new (GTK_TYPE_IMAGE, + "icon-name", icon_name, + "pixel-size", 16, + "valign", GTK_ALIGN_BASELINE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (box2), GTK_WIDGET (image)); + } + + label = g_object_new (DZL_TYPE_BOLDING_LABEL, + "label", title, + "valign", GTK_ALIGN_BASELINE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (box2), GTK_WIDGET (label)); + + return GTK_WIDGET (box); +} + +/** + * dzl_path_bar_get_path: + * + * Gets the path for the view. + * + * Returns: (transfer none): A #DzlPath. + */ +DzlPath * +dzl_path_bar_get_path (DzlPathBar *self) +{ + g_return_val_if_fail (DZL_IS_PATH_BAR (self), NULL); + + return self->path; +} + +void +dzl_path_bar_set_path (DzlPathBar *self, + DzlPath *path) +{ + g_return_if_fail (DZL_IS_PATH_BAR (self)); + + if (g_set_object (&self->path, path)) + { + gtk_container_foreach (GTK_CONTAINER (self), (GtkCallback)gtk_widget_destroy, NULL); + + if (path != NULL) + { + GList *items; + + items = dzl_path_get_elements (path); + + for (; items != NULL; items = items->next) + { + DzlPathElement *element = items->data; + GtkWidget *widget; + + widget = dzl_path_bar_create_element (self, element, items->next == NULL); + gtk_container_add (GTK_CONTAINER (self), widget); + } + } + } +} + +static void +dzl_path_bar_set_selected_index_cb (GtkWidget *widget, + gpointer user_data) +{ + struct { + GtkWidget *button; + guint index; + } *lookup = user_data; + + g_assert (GTK_IS_WIDGET (widget)); + g_assert (lookup != NULL); + + if (lookup->button == NULL && lookup->index == 0) + lookup->button = widget; + else + lookup->index--; +} + +void +dzl_path_bar_set_selected_index (DzlPathBar *self, + guint index) +{ + struct { + GtkWidget *button; + guint index; + } lookup = { NULL, index }; + + g_return_if_fail (DZL_IS_PATH_BAR (self)); + + dzl_path_bar_buttons_foreach (self, + dzl_path_bar_set_selected_index_cb, + &lookup); + + if (lookup.button) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (lookup.button), TRUE); +} diff --git a/src/pathbar/dzl-path-bar.h b/src/pathbar/dzl-path-bar.h new file mode 100644 index 0000000..a93dd63 --- /dev/null +++ b/src/pathbar/dzl-path-bar.h @@ -0,0 +1,49 @@ +/* dzl-path-bar.h + * + * Copyright (C) 2016-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PATH_BAR_H +#define DZL_PATH_BAR_H + +#include + +#include "dzl-version-macros.h" + +#include "pathbar/dzl-path.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PATH_BAR (dzl_path_bar_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlPathBar, dzl_path_bar, DZL, PATH_BAR, GtkBox) + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_path_bar_new (void); +DZL_AVAILABLE_IN_ALL +DzlPath *dzl_path_bar_get_path (DzlPathBar *self); +DZL_AVAILABLE_IN_ALL +void dzl_path_bar_set_path (DzlPathBar *self, + DzlPath *path); +DZL_AVAILABLE_IN_ALL +void dzl_path_bar_set_selected_index (DzlPathBar *self, + guint index); + +G_END_DECLS + +#endif /* DZL_PATH_BAR_H */ + diff --git a/src/pathbar/dzl-path-element.c b/src/pathbar/dzl-path-element.c new file mode 100644 index 0000000..9574bc8 --- /dev/null +++ b/src/pathbar/dzl-path-element.c @@ -0,0 +1,254 @@ +/* dzl-path-element.c + * + * Copyright (C) 2016-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-path-element" + +#include "config.h" + +#include "dzl-path-element.h" + +struct _DzlPathElement +{ + GObject parent_instance; + gchar *icon_name; + gchar *id; + gchar *title; +}; + +G_DEFINE_TYPE (DzlPathElement, dzl_path_element, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_ICON_NAME, + PROP_ID, + PROP_TITLE, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +/** + * dzl_path_element_new: + * @id: (nullable): An id for the path element. + * @icon_name: (nullable): An optional icon name for the element + * @title: The title of the element. + * + * Creates a new path element for an #DzlPath. + * + * Returns: (transfer full): A #DzlPathElement + * + * Since: 3.26 + */ +DzlPathElement * +dzl_path_element_new (const gchar *id, + const gchar *icon_name, + const gchar *title) +{ + return g_object_new (DZL_TYPE_PATH_ELEMENT, + "icon-name", icon_name, + "id", id, + "title", title, + NULL); +} + +static void +dzl_path_element_finalize (GObject *object) +{ + DzlPathElement *self = (DzlPathElement *)object; + + g_clear_pointer (&self->icon_name, g_free); + g_clear_pointer (&self->id, g_free); + g_clear_pointer (&self->title, g_free); + + G_OBJECT_CLASS (dzl_path_element_parent_class)->finalize (object); +} + +static void +dzl_path_element_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPathElement *self = DZL_PATH_ELEMENT (object); + + switch (prop_id) + { + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name); + break; + + case PROP_ID: + g_value_set_string (value, self->id); + break; + + case PROP_TITLE: + g_value_set_string (value, self->title); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_path_element_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPathElement *self = DZL_PATH_ELEMENT (object); + + switch (prop_id) + { + case PROP_ICON_NAME: + self->icon_name = g_value_dup_string (value); + break; + + case PROP_ID: + self->id = g_value_dup_string (value); + break; + + case PROP_TITLE: + self->title = g_value_dup_string (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_path_element_class_init (DzlPathElementClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_path_element_finalize; + object_class->get_property = dzl_path_element_get_property; + object_class->set_property = dzl_path_element_set_property; + + /** + * DzlPathElement:icon-name: + * + * The icon-name of the icon to display next to the path element + * in the path bar. Set to %NULL for no icon. + * + * Since: 3.26 + */ + properties [PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "Icon Name", + "The icon name for the path element", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY| G_PARAM_STATIC_STRINGS)); + + /** + * DzlPathElement:id: + * + * The id property is an application specific identifier for the + * element within the path. + * + * Since: 3.26 + */ + properties [PROP_ID] = + g_param_spec_string ("id", + "Identifier", + "Identifier", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY| G_PARAM_STATIC_STRINGS)); + + /** + * DzlPathElement:title: + * + * The title property should contain the display text that should + * be shown to represent the element in the #DzlPathBar. + * + * Since: 3.26 + */ + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "Title", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_path_element_init (DzlPathElement *self) +{ +} + +/** + * dzl_path_element_get_id: + * @self: A #DzlPathElement + * + * Gets the id for the element. Generally, a path is built of + * multiple elements and each element should have an id that + * is useful to the application that it using it. You might store + * the name of a directory, or some other key as the id. + * + * Returns: (transfer none): The id for the #DzlPathElement. + * + * Since: 3.26 + */ +const gchar * +dzl_path_element_get_id (DzlPathElement *self) +{ + g_return_val_if_fail (DZL_IS_PATH_ELEMENT (self), NULL); + + return self->id; +} + +/** + * dzl_path_element_get_icon_name: + * @self: A #DzlPathElement + * + * Gets the #DzlPathElement:icon-name property. This is used by the + * path bar to display an icon next to the element of the path. + * + * Returns: (transfer none) (nullable): The icon-name for the #DzlPathElement. + * + * Since: 3.26 + */ +const gchar * +dzl_path_element_get_icon_name (DzlPathElement *self) +{ + g_return_val_if_fail (DZL_IS_PATH_ELEMENT (self), NULL); + + return self->icon_name; +} + +/** + * dzl_path_element_get_title: + * @self: A #DzlPathElement + * + * Gets the #DzlPathElement:title property. This is used by the + * path bar to display text representing the element of the path. + * + * Returns: (transfer none) (nullable): The title for the #DzlPathElement. + * + * Since: 3.26 + */ +const gchar * +dzl_path_element_get_title (DzlPathElement *self) +{ + g_return_val_if_fail (DZL_IS_PATH_ELEMENT (self), NULL); + + return self->title; +} diff --git a/src/pathbar/dzl-path-element.h b/src/pathbar/dzl-path-element.h new file mode 100644 index 0000000..b778d5b --- /dev/null +++ b/src/pathbar/dzl-path-element.h @@ -0,0 +1,47 @@ +/* dzl-path-element.h + * + * Copyright (C) 2016-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PATH_ELEMENT_H +#define DZL_PATH_ELEMENT_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PATH_ELEMENT (dzl_path_element_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlPathElement, dzl_path_element, DZL, PATH_ELEMENT, GObject) + +DZL_AVAILABLE_IN_ALL +DzlPathElement *dzl_path_element_new (const gchar *id, + const gchar *icon_name, + const gchar *title); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_path_element_get_title (DzlPathElement *self); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_path_element_get_id (DzlPathElement *self); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_path_element_get_icon_name (DzlPathElement *self); + +G_END_DECLS + +#endif /* DZL_PATH_ELEMENT_H */ + diff --git a/src/pathbar/dzl-path.c b/src/pathbar/dzl-path.c new file mode 100644 index 0000000..15d0b61 --- /dev/null +++ b/src/pathbar/dzl-path.c @@ -0,0 +1,185 @@ +/* dzl-path.c + * + * Copyright (C) 2016-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-path" + +#include "config.h" + +#include "dzl-path.h" +#include "dzl-path-element.h" + +struct _DzlPath +{ + GObject parent_instance; + GQueue *elements; +}; + +G_DEFINE_TYPE (DzlPath, dzl_path, G_TYPE_OBJECT) + +static void +dzl_path_finalize (GObject *object) +{ + DzlPath *self = (DzlPath *)object; + + g_queue_free_full (self->elements, g_object_unref); + self->elements = NULL; + + G_OBJECT_CLASS (dzl_path_parent_class)->finalize (object); +} + +static void +dzl_path_class_init (DzlPathClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_path_finalize; +} + +static void +dzl_path_init (DzlPath *self) +{ + self->elements = g_queue_new (); +} + +/** + * dzl_path_get_elements: + * + * Returns: (transfer none) (element-type Dazzle.PathElement): The elements of the path. + */ +GList * +dzl_path_get_elements (DzlPath *self) +{ + g_return_val_if_fail (DZL_IS_PATH (self), NULL); + + return self->elements->head; +} + +DzlPath * +dzl_path_new (void) +{ + return g_object_new (DZL_TYPE_PATH, NULL); +} + +void +dzl_path_append (DzlPath *self, + DzlPathElement *element) +{ + g_return_if_fail (DZL_IS_PATH (self)); + g_return_if_fail (DZL_IS_PATH_ELEMENT (element)); + + g_queue_push_tail (self->elements, g_object_ref (element)); +} + +void +dzl_path_prepend (DzlPath *self, + DzlPathElement *element) +{ + g_return_if_fail (DZL_IS_PATH (self)); + g_return_if_fail (DZL_IS_PATH_ELEMENT (element)); + + g_queue_push_head (self->elements, g_object_ref (element)); +} + +gboolean +dzl_path_has_prefix (DzlPath *self, + DzlPath *prefix) +{ + const GList *iter; + const GList *spec; + + g_return_val_if_fail (DZL_IS_PATH (self), FALSE); + g_return_val_if_fail (DZL_IS_PATH (prefix), FALSE); + + if (self->elements->length < prefix->elements->length) + return FALSE; + + for (iter = self->elements->head, spec = prefix->elements->head; + iter != NULL && spec != NULL; + iter = iter->next, spec = spec->next) + { + DzlPathElement *spec_element = spec->data; + DzlPathElement *iter_element = iter->data; + const gchar *spec_id = dzl_path_element_get_id (spec_element); + const gchar *iter_id = dzl_path_element_get_id (iter_element); + + if (g_strcmp0 (spec_id, iter_id) == 0) + continue; + + return FALSE; + } + + return TRUE; +} + +guint +dzl_path_get_length (DzlPath *self) +{ + g_return_val_if_fail (DZL_IS_PATH (self), 0); + + return self->elements->length; +} + +gchar * +dzl_path_printf (DzlPath *self) +{ + const GList *iter; + GString *str; + + g_return_val_if_fail (DZL_IS_PATH (self), NULL); + + str = g_string_new (NULL); + + for (iter = self->elements->head; iter != NULL; iter = iter->next) + { + DzlPathElement *element = iter->data; + const gchar *id = dzl_path_element_get_id (element); + + g_string_append (str, id); + if (iter->next != NULL) + g_string_append_c (str, ','); + } + + return g_string_free (str, FALSE); +} + +gboolean +dzl_path_is_empty (DzlPath *self) +{ + g_return_val_if_fail (DZL_IS_PATH (self), FALSE); + + return self->elements->length == 0; +} + +/** + * dzl_path_get_element: + * + * Gets the path element found at @index. + * + * Indexes start from zero. + * + * Returns: (nullable) (transfer none): An #DzlPathElement. + */ +DzlPathElement * +dzl_path_get_element (DzlPath *self, + guint index) +{ + g_return_val_if_fail (DZL_IS_PATH (self), NULL); + g_return_val_if_fail (index < self->elements->length, NULL); + + return g_queue_peek_nth (self->elements, index); +} diff --git a/src/pathbar/dzl-path.h b/src/pathbar/dzl-path.h new file mode 100644 index 0000000..1db955d --- /dev/null +++ b/src/pathbar/dzl-path.h @@ -0,0 +1,60 @@ +/* dzl-path.h + * + * Copyright (C) 2016-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PATH_H +#define DZL_PATH_H + +#include + +#include "dzl-version-macros.h" + +#include "pathbar/dzl-path-element.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PATH (dzl_path_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlPath, dzl_path, DZL, PATH, GObject) + +DZL_AVAILABLE_IN_ALL +DzlPath *dzl_path_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_path_prepend (DzlPath *self, + DzlPathElement *element); +DZL_AVAILABLE_IN_ALL +void dzl_path_append (DzlPath *self, + DzlPathElement *element); +DZL_AVAILABLE_IN_ALL +GList *dzl_path_get_elements (DzlPath *self); +DZL_AVAILABLE_IN_ALL +gboolean dzl_path_has_prefix (DzlPath *self, + DzlPath *prefix); +DZL_AVAILABLE_IN_ALL +guint dzl_path_get_length (DzlPath *self); +DZL_AVAILABLE_IN_ALL +DzlPathElement *dzl_path_get_element (DzlPath *self, + guint index); +DZL_AVAILABLE_IN_ALL +gchar *dzl_path_printf (DzlPath *self); +DZL_AVAILABLE_IN_ALL +gboolean dzl_path_is_empty (DzlPath *self); + +G_END_DECLS + +#endif /* DZL_PATH_H */ diff --git a/src/pathbar/meson.build b/src/pathbar/meson.build new file mode 100644 index 0000000..7234b58 --- /dev/null +++ b/src/pathbar/meson.build @@ -0,0 +1,16 @@ +pathbar_headers = [ + 'dzl-path-bar.h', + 'dzl-path-element.h', + 'dzl-path.h', +] + +pathbar_sources = [ + 'dzl-path-bar.c', + 'dzl-path.c', + 'dzl-path-element.c', +] + +libdazzle_public_headers += files(pathbar_headers) +libdazzle_public_sources += files(pathbar_sources) + +install_headers(pathbar_headers, subdir: join_paths(libdazzle_header_subdir, 'pathbar')) diff --git a/src/prefs/dzl-preferences-bin-private.h b/src/prefs/dzl-preferences-bin-private.h new file mode 100644 index 0000000..fbb835c --- /dev/null +++ b/src/prefs/dzl-preferences-bin-private.h @@ -0,0 +1,33 @@ +/* dzl-preferences-bin-private.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_BIN_PRIVATE_H +#define DZL_PREFERENCES_BIN_PRIVATE_H + +#include "prefs/dzl-preferences-bin.h" + +G_BEGIN_DECLS + +void _dzl_preferences_bin_set_map (DzlPreferencesBin *self, + GHashTable *map); +gboolean _dzl_preferences_bin_matches (DzlPreferencesBin *self, + DzlPatternSpec *spec); + +G_END_DECLS + +#endif /* DZL_PREFERENCES_BIN_PRIVATE_H */ diff --git a/src/prefs/dzl-preferences-bin.c b/src/prefs/dzl-preferences-bin.c new file mode 100644 index 0000000..f578495 --- /dev/null +++ b/src/prefs/dzl-preferences-bin.c @@ -0,0 +1,450 @@ +/* dzl-preferences-bin.c + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-preferences-bin" + +#include "config.h" + +#include + +#include "prefs/dzl-preferences-bin.h" + +typedef struct +{ + GtkBin parent_instance; + + gint priority; + + gchar *keywords; + gchar *schema_id; + gchar *path; + GSettings *settings; + GHashTable *map; +} DzlPreferencesBinPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlPreferencesBin, dzl_preferences_bin, GTK_TYPE_BIN) + +enum { + PROP_0, + PROP_KEYWORDS, + PROP_PRIORITY, + PROP_SCHEMA_ID, + PROP_PATH, + LAST_PROP +}; + +enum { + PREFERENCE_ACTIVATED, + N_SIGNALS +}; + +static guint signals [N_SIGNALS]; + +static GParamSpec *properties [LAST_PROP]; +static GHashTable *settings_cache; + +static gchar * +dzl_preferences_bin_expand (DzlPreferencesBin *self, + const gchar *spec) +{ + DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self); + GHashTableIter iter; + const gchar *key; + const gchar *value; + gchar *expanded; + + g_assert (DZL_IS_PREFERENCES_BIN (self)); + + if (spec == NULL) + return NULL; + + expanded = g_strdup (spec); + + if (priv->map == NULL) + goto validate; + + g_hash_table_iter_init (&iter, priv->map); + + while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value)) + { + gchar *tmp = expanded; + gchar **split; + + split = g_strsplit (tmp, key, 0); + expanded = g_strjoinv (value, split); + + g_strfreev (split); + g_free (tmp); + } + +validate: + if (strchr (expanded, '{') != NULL) + { + g_free (expanded); + return NULL; + } + + return expanded; +} + +static void +dzl_preferences_bin_evict_settings (gpointer data, + GObject *where_object_was) +{ + g_assert (data != NULL); + g_assert (where_object_was != NULL); + + g_hash_table_remove (settings_cache, (gchar *)data); +} + +static void +dzl_preferences_bin_cache_settings (const gchar *hash_key, + GSettings *settings) +{ + gchar *key; + + g_assert (hash_key != NULL); + g_assert (G_IS_SETTINGS (settings)); + + key = g_strdup (hash_key); + g_hash_table_insert (settings_cache, key, settings); + g_object_weak_ref (G_OBJECT (settings), dzl_preferences_bin_evict_settings, key); +} + +static GSettings * +dzl_preferences_bin_get_settings (DzlPreferencesBin *self) +{ + DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_PREFERENCES_BIN (self), NULL); + + if (priv->settings == NULL) + { + g_autofree gchar *resolved_schema_id = NULL; + g_autofree gchar *resolved_path = NULL; + g_autofree gchar *hash_key = NULL; + + resolved_schema_id = dzl_preferences_bin_expand (self, priv->schema_id); + resolved_path = dzl_preferences_bin_expand (self, priv->path); + + if (resolved_schema_id == NULL) + return NULL; + + if ((priv->path != NULL) && (resolved_path == NULL)) + return NULL; + + hash_key = g_strdup_printf ("%s|%s", + resolved_schema_id, + resolved_path ?: ""); + + if (!g_hash_table_contains (settings_cache, hash_key)) + { + GSettingsSchemaSource *source; + GSettingsSchema *schema; + + source = g_settings_schema_source_get_default (); + schema = g_settings_schema_source_lookup (source, resolved_schema_id, TRUE); + + if (schema != NULL) + { + if (resolved_path) + priv->settings = g_settings_new_with_path (resolved_schema_id, resolved_path); + else + priv->settings = g_settings_new (resolved_schema_id); + dzl_preferences_bin_cache_settings (hash_key, priv->settings); + } + + g_clear_pointer (&schema, g_settings_schema_unref); + } + else + { + priv->settings = g_object_ref (g_hash_table_lookup (settings_cache, hash_key)); + } + + g_clear_pointer (&hash_key, g_free); + g_clear_pointer (&resolved_schema_id, g_free); + g_clear_pointer (&resolved_path, g_free); + } + + return (priv->settings != NULL) ? g_object_ref (priv->settings) : NULL; +} + + +static void +dzl_preferences_bin_connect (DzlPreferencesBin *self, + GSettings *settings) +{ + g_assert (DZL_IS_PREFERENCES_BIN (self)); + g_assert (G_IS_SETTINGS (settings)); + + if (DZL_PREFERENCES_BIN_GET_CLASS (self)->connect != NULL) + DZL_PREFERENCES_BIN_GET_CLASS (self)->connect (self, settings); +} + +static void +dzl_preferences_bin_disconnect (DzlPreferencesBin *self, + GSettings *settings) +{ + g_assert (DZL_IS_PREFERENCES_BIN (self)); + g_assert (G_IS_SETTINGS (settings)); + + if (DZL_PREFERENCES_BIN_GET_CLASS (self)->disconnect != NULL) + DZL_PREFERENCES_BIN_GET_CLASS (self)->disconnect (self, settings); +} + +static void +dzl_preferences_bin_reload (DzlPreferencesBin *self) +{ + DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self); + GSettings *settings; + + g_assert (DZL_IS_PREFERENCES_BIN (self)); + + if (priv->settings != NULL) + { + dzl_preferences_bin_disconnect (self, priv->settings); + g_clear_object (&priv->settings); + } + + settings = dzl_preferences_bin_get_settings (self); + + if (settings != NULL) + { + dzl_preferences_bin_connect (self, settings); + g_object_unref (settings); + } +} + +static void +dzl_preferences_bin_constructed (GObject *object) +{ + DzlPreferencesBin *self = (DzlPreferencesBin *)object; + + G_OBJECT_CLASS (dzl_preferences_bin_parent_class)->constructed (object); + + dzl_preferences_bin_reload (self); +} + +static void +dzl_preferences_bin_destroy (GtkWidget *widget) +{ + DzlPreferencesBin *self = (DzlPreferencesBin *)widget; + DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self); + + g_assert (DZL_IS_PREFERENCES_BIN (self)); + + if (priv->settings != NULL) + { + dzl_preferences_bin_disconnect (self, priv->settings); + g_clear_object (&priv->settings); + } + + GTK_WIDGET_CLASS (dzl_preferences_bin_parent_class)->destroy (widget); +} + +static void +dzl_preferences_bin_activated (GtkWidget *widget) +{ + GtkWidget *child = gtk_bin_get_child (GTK_BIN (widget)); + + if (child) + gtk_widget_activate (child); +} + +static void +dzl_preferences_bin_finalize (GObject *object) +{ + DzlPreferencesBin *self = (DzlPreferencesBin *)object; + DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self); + + g_clear_pointer (&priv->schema_id, g_free); + g_clear_pointer (&priv->path, g_free); + g_clear_pointer (&priv->keywords, g_free); + g_clear_pointer (&priv->map, g_hash_table_unref); + g_clear_object (&priv->settings); + + G_OBJECT_CLASS (dzl_preferences_bin_parent_class)->finalize (object); +} + +static void +dzl_preferences_bin_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesBin *self = DZL_PREFERENCES_BIN (object); + DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self); + + switch (prop_id) + { + case PROP_SCHEMA_ID: + g_value_set_string (value, priv->schema_id); + break; + + case PROP_PATH: + g_value_set_string (value, priv->path); + break; + + case PROP_KEYWORDS: + g_value_set_string (value, priv->keywords); + break; + + case PROP_PRIORITY: + g_value_set_int (value, priv->priority); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_bin_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesBin *self = DZL_PREFERENCES_BIN (object); + DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self); + + switch (prop_id) + { + case PROP_SCHEMA_ID: + priv->schema_id = g_value_dup_string (value); + break; + + case PROP_PATH: + priv->path = g_value_dup_string (value); + break; + + case PROP_KEYWORDS: + priv->keywords = g_value_dup_string (value); + break; + + case PROP_PRIORITY: + priv->priority = g_value_get_int (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_bin_class_init (DzlPreferencesBinClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = dzl_preferences_bin_constructed; + object_class->finalize = dzl_preferences_bin_finalize; + object_class->get_property = dzl_preferences_bin_get_property; + object_class->set_property = dzl_preferences_bin_set_property; + + widget_class->destroy = dzl_preferences_bin_destroy; + + properties [PROP_KEYWORDS] = + g_param_spec_string ("keywords", + "Keywords", + "Search keywords for the widget.", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_PATH] = + g_param_spec_string ("path", + "Path", + "Path", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_PRIORITY] = + g_param_spec_int ("priority", + "Priority", + "The widget priority within the group.", + G_MININT, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SCHEMA_ID] = + g_param_spec_string ("schema-id", + "Schema Id", + "Schema Id", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + signals [PREFERENCE_ACTIVATED] = widget_class->activate_signal = + g_signal_new_class_handler ("preference-activated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_CALLBACK (dzl_preferences_bin_activated), + NULL, NULL, NULL, G_TYPE_NONE, 0); + + gtk_widget_class_set_css_name (widget_class, "preferencesbin"); + + settings_cache = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); +} + +static void +dzl_preferences_bin_init (DzlPreferencesBin *self) +{ +} + +void +_dzl_preferences_bin_set_map (DzlPreferencesBin *self, + GHashTable *map) +{ + DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self); + + g_return_if_fail (DZL_IS_PREFERENCES_BIN (self)); + + if (map != priv->map) + { + g_clear_pointer (&priv->map, g_hash_table_unref); + priv->map = map ? g_hash_table_ref (map) : NULL; + dzl_preferences_bin_reload (self); + } +} + +gboolean +_dzl_preferences_bin_matches (DzlPreferencesBin *self, + DzlPatternSpec *spec) +{ + DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_PREFERENCES_BIN (self), FALSE); + + if (spec == NULL) + return TRUE; + + if (priv->keywords && dzl_pattern_spec_match (spec, priv->keywords)) + return TRUE; + + if (priv->schema_id && dzl_pattern_spec_match (spec, priv->schema_id)) + return TRUE; + + if (priv->path && dzl_pattern_spec_match (spec, priv->path)) + return TRUE; + + if (DZL_PREFERENCES_BIN_GET_CLASS (self)->matches) + return DZL_PREFERENCES_BIN_GET_CLASS (self)->matches (self, spec); + + return FALSE; +} diff --git a/src/prefs/dzl-preferences-bin.h b/src/prefs/dzl-preferences-bin.h new file mode 100644 index 0000000..ddcea41 --- /dev/null +++ b/src/prefs/dzl-preferences-bin.h @@ -0,0 +1,58 @@ +/* dzl-preferences-bin.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_BIN_H +#define DZL_PREFERENCES_BIN_H + +#include + +#include "dzl-version-macros.h" + +#include "search/dzl-pattern-spec.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PREFERENCES_BIN (dzl_preferences_bin_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlPreferencesBin, dzl_preferences_bin, DZL, PREFERENCES_BIN, GtkBin) + +struct _DzlPreferencesBinClass +{ + GtkBinClass parent_class; + + void (*connect) (DzlPreferencesBin *self, + GSettings *settings); + void (*disconnect) (DzlPreferencesBin *self, + GSettings *settings); + gboolean (*matches) (DzlPreferencesBin *self, + DzlPatternSpec *spec); + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +G_END_DECLS + +#endif /* DZL_PREFERENCES_BIN_H */ diff --git a/src/prefs/dzl-preferences-entry.c b/src/prefs/dzl-preferences-entry.c new file mode 100644 index 0000000..b8f84ea --- /dev/null +++ b/src/prefs/dzl-preferences-entry.c @@ -0,0 +1,235 @@ +/* dzl-preferences-entry.c + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-preferences-entry" + +#include "config.h" + +#include + +#include "prefs/dzl-preferences-entry.h" + +typedef struct +{ + GtkEntry *entry; + GtkLabel *title; +} DzlPreferencesEntryPrivate; + +enum { + PROP_0, + PROP_TITLE, + PROP_TEXT, + LAST_PROP +}; + +enum { + ACTIVATE, + CHANGED, + LAST_SIGNAL +}; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlPreferencesEntry, dzl_preferences_entry, DZL_TYPE_PREFERENCES_BIN) + +static GParamSpec *properties [LAST_PROP]; +static guint signals [LAST_SIGNAL]; + +static void +dzl_preferences_entry_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesEntry *self = DZL_PREFERENCES_ENTRY (object); + DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self); + + switch (prop_id) + { + case PROP_TEXT: + g_value_set_string (value, gtk_entry_get_text (priv->entry)); + break; + + case PROP_TITLE: + g_value_set_string (value, gtk_label_get_text (priv->title)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_entry_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesEntry *self = DZL_PREFERENCES_ENTRY (object); + DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self); + + switch (prop_id) + { + case PROP_TEXT: + gtk_entry_set_text (priv->entry, g_value_get_string (value)); + break; + + case PROP_TITLE: + gtk_label_set_label (priv->title, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_entry_activate (DzlPreferencesEntry *self) +{ + DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self); + + g_assert (DZL_IS_PREFERENCES_ENTRY (self)); + + gtk_widget_grab_focus (GTK_WIDGET (priv->entry)); +} + +static void +dzl_preferences_entry_changed (DzlPreferencesEntry *self, + GtkEntry *entry) +{ + const gchar *text; + + g_assert (DZL_IS_PREFERENCES_ENTRY (self)); + g_assert (GTK_IS_ENTRY (entry)); + + text = gtk_entry_get_text (entry); + g_signal_emit (self, signals [CHANGED], 0, text); +} + +static gboolean +dzl_preferences_entry_matches (DzlPreferencesBin *bin, + DzlPatternSpec *spec) +{ + DzlPreferencesEntry *self = (DzlPreferencesEntry *)bin; + DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self); + const gchar *tmp; + + g_assert (DZL_IS_PREFERENCES_ENTRY (self)); + g_assert (spec != NULL); + + tmp = gtk_label_get_label (priv->title); + if (tmp && dzl_pattern_spec_match (spec, tmp)) + return TRUE; + + tmp = gtk_entry_get_text (GTK_ENTRY (priv->entry)); + if (tmp && dzl_pattern_spec_match (spec, tmp)) + return TRUE; + + return FALSE; +} + +static void +dzl_preferences_entry_class_init (DzlPreferencesEntryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + DzlPreferencesBinClass *bin_class = DZL_PREFERENCES_BIN_CLASS (klass); + + object_class->get_property = dzl_preferences_entry_get_property; + object_class->set_property = dzl_preferences_entry_set_property; + + bin_class->matches = dzl_preferences_entry_matches; + + signals [ACTIVATE] = + g_signal_new_class_handler ("activate", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_CALLBACK (dzl_preferences_entry_activate), + NULL, NULL, NULL, G_TYPE_NONE, 0); + + signals [CHANGED] = + g_signal_new_class_handler ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + NULL, NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_STRING); + + widget_class->activate_signal = signals [ACTIVATE]; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-preferences-entry.ui"); + gtk_widget_class_bind_template_child_private (widget_class, DzlPreferencesEntry, entry); + gtk_widget_class_bind_template_child_private (widget_class, DzlPreferencesEntry, title); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "Title", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TEXT] = + g_param_spec_string ("text", + "Text", + "Text", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_preferences_entry_init (DzlPreferencesEntry *self) +{ + DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); + + g_signal_connect_object (priv->entry, + "changed", + G_CALLBACK (dzl_preferences_entry_changed), + self, + G_CONNECT_SWAPPED); +} + +/** + * dzl_preferences_entry_get_title_widget: + * + * Returns: (transfer none): A #GtkWidget + */ +GtkWidget * +dzl_preferences_entry_get_title_widget (DzlPreferencesEntry *self) +{ + DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_PREFERENCES_ENTRY (self), NULL); + + return GTK_WIDGET (priv->title); +} + +/** + * dzl_preferences_entry_get_entry_widget: + * + * Returns: (transfer none): A #GtkWidget + */ +GtkWidget * +dzl_preferences_entry_get_entry_widget (DzlPreferencesEntry *self) +{ + DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_PREFERENCES_ENTRY (self), NULL); + + return GTK_WIDGET (priv->entry); +} diff --git a/src/prefs/dzl-preferences-entry.h b/src/prefs/dzl-preferences-entry.h new file mode 100644 index 0000000..f5a6682 --- /dev/null +++ b/src/prefs/dzl-preferences-entry.h @@ -0,0 +1,47 @@ +/* dzl-preferences-entry.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_ENTRY_H +#define DZL_PREFERENCES_ENTRY_H + +#include + +#include "dzl-version-macros.h" + +#include "prefs/dzl-preferences-bin.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PREFERENCES_ENTRY (dzl_preferences_entry_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlPreferencesEntry, dzl_preferences_entry, DZL, PREFERENCES_ENTRY, DzlPreferencesBin) + +struct _DzlPreferencesEntryClass +{ + DzlPreferencesBinClass parent_class; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_preferences_entry_get_entry_widget (DzlPreferencesEntry *self); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_preferences_entry_get_title_widget (DzlPreferencesEntry *self); + +G_END_DECLS + +#endif /* DZL_PREFERENCES_ENTRY_H */ diff --git a/src/prefs/dzl-preferences-entry.ui b/src/prefs/dzl-preferences-entry.ui new file mode 100644 index 0000000..55f10ba --- /dev/null +++ b/src/prefs/dzl-preferences-entry.ui @@ -0,0 +1,26 @@ + + + + + diff --git a/src/prefs/dzl-preferences-file-chooser-button.c b/src/prefs/dzl-preferences-file-chooser-button.c new file mode 100644 index 0000000..960429b --- /dev/null +++ b/src/prefs/dzl-preferences-file-chooser-button.c @@ -0,0 +1,249 @@ +/* dzl-preferences-file-chooser-button.c + * + * Copyright (C) 2016 Akshaya Kakkilaya + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-preferences-file-chooser-button" + +#include "config.h" + +#include "util/dzl-util-private.h" +#include "prefs/dzl-preferences-file-chooser-button.h" + +struct _DzlPreferencesFileChooserButton +{ + DzlPreferencesBin parent_instance; + + gchar *key; + GSettings *settings; + + GtkFileChooserButton *widget; + GtkLabel *title; + GtkLabel *subtitle; +}; + +G_DEFINE_TYPE (DzlPreferencesFileChooserButton, dzl_preferences_file_chooser_button, DZL_TYPE_PREFERENCES_BIN) + +enum { + PROP_0, + PROP_ACTION, + PROP_KEY, + PROP_SUBTITLE, + PROP_TITLE, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +static void +dzl_preferences_file_chooser_button_save_file (DzlPreferencesFileChooserButton *self, + GtkFileChooserButton *widget) +{ + g_autofree gchar *path = NULL; + + g_assert (DZL_IS_PREFERENCES_FILE_CHOOSER_BUTTON (self)); + + path = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (self->widget)); + + g_settings_set_string (self->settings, self->key, path); + +} + +static void +dzl_preferences_file_chooser_button_connect (DzlPreferencesBin *bin, + GSettings *settings) +{ + DzlPreferencesFileChooserButton *self = (DzlPreferencesFileChooserButton *)bin; + g_autofree gchar *file = NULL; + g_autofree gchar *path = NULL; + + g_assert (DZL_IS_PREFERENCES_FILE_CHOOSER_BUTTON (self)); + g_assert (G_IS_SETTINGS (settings)); + + self->settings = g_object_ref (settings); + + file = g_settings_get_string (settings, self->key); + + if (!dzl_str_empty0 (file)) + { + if (!g_path_is_absolute (file)) + path = g_build_filename (g_get_home_dir (), file, NULL); + else + path = g_steal_pointer (&file); + + gtk_file_chooser_set_filename (GTK_FILE_CHOOSER (self->widget), path); + } + + g_signal_connect_object (self->widget, + "file-set", + G_CALLBACK (dzl_preferences_file_chooser_button_save_file), + self, + G_CONNECT_SWAPPED); +} + +static gboolean +dzl_preferences_file_chooser_button_matches (DzlPreferencesBin *bin, + DzlPatternSpec *spec) +{ + DzlPreferencesFileChooserButton *self = (DzlPreferencesFileChooserButton *)bin; + const gchar *tmp; + + g_assert (DZL_IS_PREFERENCES_FILE_CHOOSER_BUTTON (self)); + g_assert (spec != NULL); + + tmp = gtk_label_get_label (self->title); + if (tmp && dzl_pattern_spec_match (spec, tmp)) + return TRUE; + + tmp = gtk_label_get_label (self->subtitle); + if (tmp && dzl_pattern_spec_match (spec, tmp)) + return TRUE; + + if (self->key && dzl_pattern_spec_match (spec, self->key)) + return TRUE; + + return FALSE; +} + +static void +dzl_preferences_file_chooser_button_finalize (GObject *object) +{ + DzlPreferencesFileChooserButton *self = (DzlPreferencesFileChooserButton *)object; + + g_clear_pointer (&self->key, g_free); + g_clear_object (&self->settings); + + G_OBJECT_CLASS (dzl_preferences_file_chooser_button_parent_class)->finalize (object); +} + +static void +dzl_preferences_file_chooser_button_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesFileChooserButton *self = DZL_PREFERENCES_FILE_CHOOSER_BUTTON (object); + + switch (prop_id) + { + case PROP_ACTION: + g_value_set_enum (value, gtk_file_chooser_get_action (GTK_FILE_CHOOSER (self->widget))); + break; + + case PROP_KEY: + g_value_set_string (value, self->key); + break; + + case PROP_TITLE: + g_value_set_string (value, gtk_label_get_label (self->title)); + break; + + case PROP_SUBTITLE: + g_value_set_string (value, gtk_label_get_label (self->title)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_file_chooser_button_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesFileChooserButton *self = DZL_PREFERENCES_FILE_CHOOSER_BUTTON (object); + + switch (prop_id) + { + case PROP_ACTION: + gtk_file_chooser_set_action (GTK_FILE_CHOOSER (self->widget), g_value_get_enum (value)); + break; + + case PROP_KEY: + self->key = g_value_dup_string (value); + break; + + case PROP_TITLE: + gtk_label_set_label (self->title, g_value_get_string (value)); + break; + + case PROP_SUBTITLE: + gtk_label_set_label (self->subtitle, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_file_chooser_button_class_init (DzlPreferencesFileChooserButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + DzlPreferencesBinClass *bin_class = DZL_PREFERENCES_BIN_CLASS (klass); + + object_class->finalize = dzl_preferences_file_chooser_button_finalize; + object_class->get_property = dzl_preferences_file_chooser_button_get_property; + object_class->set_property = dzl_preferences_file_chooser_button_set_property; + + bin_class->connect = dzl_preferences_file_chooser_button_connect; + bin_class->matches = dzl_preferences_file_chooser_button_matches; + + properties [PROP_ACTION] = + g_param_spec_enum ("action", + "Action", + "Action", + GTK_TYPE_FILE_CHOOSER_ACTION, + GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_KEY] = + g_param_spec_string ("key", + "Key", + "Key", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "Title", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SUBTITLE] = + g_param_spec_string ("subtitle", + "Subtitle", + "Subtitle", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-preferences-file-chooser-button.ui"); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFileChooserButton, widget); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFileChooserButton, title); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFileChooserButton, subtitle); +} + +static void +dzl_preferences_file_chooser_button_init (DzlPreferencesFileChooserButton *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} diff --git a/src/prefs/dzl-preferences-file-chooser-button.h b/src/prefs/dzl-preferences-file-chooser-button.h new file mode 100644 index 0000000..0fa9969 --- /dev/null +++ b/src/prefs/dzl-preferences-file-chooser-button.h @@ -0,0 +1,35 @@ +/* dzl-preferences-file-chooser-button.h + * + * Copyright (C) 2016 Akshaya Kakkilaya + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_FILE_CHOOSER_BUTTON_H +#define DZL_PREFERENCES_FILE_CHOOSER_BUTTON_H + +#include "dzl-version-macros.h" + +#include "prefs/dzl-preferences-bin.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PREFERENCES_FILE_CHOOSER_BUTTON (dzl_preferences_file_chooser_button_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlPreferencesFileChooserButton, dzl_preferences_file_chooser_button, DZL, PREFERENCES_FILE_CHOOSER_BUTTON, DzlPreferencesBin) + +G_END_DECLS + +#endif /* DZL_PREFERENCES_FILE_CHOOSER_BUTTON_H */ diff --git a/src/prefs/dzl-preferences-file-chooser-button.ui b/src/prefs/dzl-preferences-file-chooser-button.ui new file mode 100644 index 0000000..0ac6142 --- /dev/null +++ b/src/prefs/dzl-preferences-file-chooser-button.ui @@ -0,0 +1,49 @@ + + + + + diff --git a/src/prefs/dzl-preferences-flow-box.c b/src/prefs/dzl-preferences-flow-box.c new file mode 100644 index 0000000..923f00e --- /dev/null +++ b/src/prefs/dzl-preferences-flow-box.c @@ -0,0 +1,40 @@ +/* dzl-preferences-flow-box.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-preferences-flow-box" + +#include "config.h" + +#include "prefs/dzl-preferences-flow-box.h" + +struct _DzlPreferencesFlowBox +{ + DzlColumnLayout parent; +}; + +G_DEFINE_TYPE (DzlPreferencesFlowBox, dzl_preferences_flow_box, DZL_TYPE_COLUMN_LAYOUT) + +static void +dzl_preferences_flow_box_class_init (DzlPreferencesFlowBoxClass *klass) +{ +} + +static void +dzl_preferences_flow_box_init (DzlPreferencesFlowBox *self) +{ +} diff --git a/src/prefs/dzl-preferences-flow-box.h b/src/prefs/dzl-preferences-flow-box.h new file mode 100644 index 0000000..f981b67 --- /dev/null +++ b/src/prefs/dzl-preferences-flow-box.h @@ -0,0 +1,38 @@ +/* dzl-preferences-flow-box.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_FLOW_BOX_H +#define DZL_PREFERENCES_FLOW_BOX_H + +#include "dzl-version-macros.h" + +#include "widgets/dzl-column-layout.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PREFERENCES_FLOW_BOX (dzl_preferences_flow_box_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlPreferencesFlowBox, dzl_preferences_flow_box, DZL, PREFERENCES_FLOW_BOX, DzlColumnLayout) + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_preferences_flow_box_new (void); + +G_END_DECLS + +#endif /* DZL_PREFERENCES_FLOW_BOX_H */ diff --git a/src/prefs/dzl-preferences-font-button.c b/src/prefs/dzl-preferences-font-button.c new file mode 100644 index 0000000..25d7877 --- /dev/null +++ b/src/prefs/dzl-preferences-font-button.c @@ -0,0 +1,331 @@ +/* dzl-preferences-font-button.c + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-preferences-font-button" + +#include "config.h" + +#include "prefs/dzl-preferences-font-button.h" + +struct _DzlPreferencesFontButton +{ + GtkBin parent_instance; + + gulong handler; + + GSettings *settings; + gchar *key; + + GtkLabel *title; + GtkLabel *font_family; + GtkLabel *font_size; + GtkPopover *popover; + GtkButton *confirm; + GtkFontChooserWidget *chooser; +}; + +G_DEFINE_TYPE (DzlPreferencesFontButton, dzl_preferences_font_button, DZL_TYPE_PREFERENCES_BIN) + +enum { + PROP_0, + PROP_KEY, + PROP_TITLE, + LAST_PROP +}; + +enum { + ACTIVATE, + LAST_SIGNAL +}; + +static GParamSpec *properties [LAST_PROP]; +static guint signals [LAST_SIGNAL]; + +static void +dzl_preferences_font_button_show (DzlPreferencesFontButton *self) +{ + gchar *font = NULL; + + g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self)); + + font = g_settings_get_string (self->settings, self->key); + g_object_set (self->chooser, "font", font, NULL); + g_free (font); + + gtk_popover_popup (self->popover); +} + +static void +dzl_preferences_font_button_activate (DzlPreferencesFontButton *self) +{ + g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self)); + + if (!gtk_widget_get_visible (GTK_WIDGET (self->popover))) + dzl_preferences_font_button_show (self); +} + +static void +dzl_preferences_font_button_changed (DzlPreferencesFontButton *self, + const gchar *key, + GSettings *settings) +{ + PangoFontDescription *font_desc; + gchar *name; + + g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self)); + g_assert (key != NULL); + g_assert (G_IS_SETTINGS (settings)); + + name = g_settings_get_string (settings, key); + font_desc = pango_font_description_from_string (name); + + if (font_desc != NULL) + { + gchar *font_size; + + gtk_label_set_label (self->font_family, pango_font_description_get_family (font_desc)); + font_size = g_strdup_printf ("%d", pango_font_description_get_size (font_desc) / PANGO_SCALE); + gtk_label_set_label (self->font_size, font_size); + g_free (font_size); + } + + g_clear_pointer (&font_desc, pango_font_description_free); + g_free (name); +} + +static void +dzl_preferences_font_button_connect (DzlPreferencesBin *bin, + GSettings *settings) +{ + DzlPreferencesFontButton *self = (DzlPreferencesFontButton *)bin; + g_autofree gchar *signal_detail = NULL; + + g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self)); + + signal_detail = g_strdup_printf ("changed::%s", self->key); + + self->settings = g_object_ref (settings); + + self->handler = + g_signal_connect_object (settings, + signal_detail, + G_CALLBACK (dzl_preferences_font_button_changed), + self, + G_CONNECT_SWAPPED); + + dzl_preferences_font_button_changed (self, self->key, settings); +} + +static void +dzl_preferences_font_button_disconnect (DzlPreferencesBin *bin, + GSettings *settings) +{ + DzlPreferencesFontButton *self = (DzlPreferencesFontButton *)bin; + + g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self)); + + g_signal_handler_disconnect (settings, self->handler); + self->handler = 0; +} + +static gboolean +dzl_preferences_font_button_matches (DzlPreferencesBin *bin, + DzlPatternSpec *spec) +{ + DzlPreferencesFontButton *self = (DzlPreferencesFontButton *)bin; + const gchar *tmp; + + g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self)); + g_assert (spec != NULL); + + tmp = gtk_label_get_label (self->title); + if (tmp && dzl_pattern_spec_match (spec, tmp)) + return TRUE; + + tmp = gtk_label_get_label (self->font_family); + if (tmp && dzl_pattern_spec_match (spec, tmp)) + return TRUE; + + return FALSE; +} + +static void +dzl_preferences_font_button_finalize (GObject *object) +{ + DzlPreferencesFontButton *self = (DzlPreferencesFontButton *)object; + + g_clear_object (&self->settings); + g_clear_pointer (&self->key, g_free); + + G_OBJECT_CLASS (dzl_preferences_font_button_parent_class)->finalize (object); +} + +static void +dzl_preferences_font_button_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesFontButton *self = DZL_PREFERENCES_FONT_BUTTON (object); + + switch (prop_id) + { + case PROP_KEY: + g_value_set_string (value, self->key); + break; + + case PROP_TITLE: + g_value_set_string (value, gtk_label_get_label (self->title)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_font_button_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesFontButton *self = DZL_PREFERENCES_FONT_BUTTON (object); + + switch (prop_id) + { + case PROP_KEY: + self->key = g_value_dup_string (value); + break; + + case PROP_TITLE: + gtk_label_set_label (self->title, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_font_button_class_init (DzlPreferencesFontButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + DzlPreferencesBinClass *bin_class = DZL_PREFERENCES_BIN_CLASS (klass); + + object_class->finalize = dzl_preferences_font_button_finalize; + object_class->get_property = dzl_preferences_font_button_get_property; + object_class->set_property = dzl_preferences_font_button_set_property; + + bin_class->connect = dzl_preferences_font_button_connect; + bin_class->disconnect = dzl_preferences_font_button_disconnect; + bin_class->matches = dzl_preferences_font_button_matches; + + signals [ACTIVATE] = + g_signal_new_class_handler ("activate", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_CALLBACK (dzl_preferences_font_button_activate), + NULL, NULL, NULL, G_TYPE_NONE, 0); + + widget_class->activate_signal = signals [ACTIVATE]; + + properties [PROP_KEY] = + g_param_spec_string ("key", + "Key", + "Key", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "Title", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-preferences-font-button.ui"); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFontButton, chooser); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFontButton, confirm); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFontButton, font_family); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFontButton, font_size); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFontButton, popover); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFontButton, title); +} + +static gboolean +transform_to (GBinding *binding, + const GValue *value, + GValue *to_value, + gpointer user_data) +{ + g_value_set_boolean (to_value, !!g_value_get_boxed (value)); + return TRUE; +} + +static void +dzl_preferences_font_button_clicked (DzlPreferencesFontButton *self, + GtkButton *button) +{ + g_autofree gchar *font = NULL; + + g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self)); + g_assert (GTK_IS_BUTTON (button)); + + g_object_get (self->chooser, "font", &font, NULL); + g_settings_set_string (self->settings, self->key, font); + gtk_popover_popdown (self->popover); +} + +static void +dzl_preferences_font_button_font_activated (DzlPreferencesFontButton *self, + const gchar *font, + GtkFontChooser *chooser) +{ + g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self)); + g_assert (GTK_IS_FONT_CHOOSER (chooser)); + + g_settings_set_string (self->settings, self->key, font); + gtk_popover_popdown (self->popover); +} + +static void +dzl_preferences_font_button_init (DzlPreferencesFontButton *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + g_object_bind_property_full (self->chooser, "font-desc", + self->confirm, "sensitive", + G_BINDING_SYNC_CREATE, + transform_to, + NULL, NULL, NULL); + + g_signal_connect_object (self->chooser, + "font-activated", + G_CALLBACK (dzl_preferences_font_button_font_activated), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (self->confirm, + "clicked", + G_CALLBACK (dzl_preferences_font_button_clicked), + self, + G_CONNECT_SWAPPED); +} diff --git a/src/prefs/dzl-preferences-font-button.h b/src/prefs/dzl-preferences-font-button.h new file mode 100644 index 0000000..f2d5179 --- /dev/null +++ b/src/prefs/dzl-preferences-font-button.h @@ -0,0 +1,35 @@ +/* dzl-preferences-font-button.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_FONT_BUTTON_H +#define DZL_PREFERENCES_FONT_BUTTON_H + +#include "dzl-version-macros.h" + +#include "prefs/dzl-preferences-bin.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PREFERENCES_FONT_BUTTON (dzl_preferences_font_button_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlPreferencesFontButton, dzl_preferences_font_button, DZL, PREFERENCES_FONT_BUTTON, DzlPreferencesBin) + +G_END_DECLS + +#endif /* DZL_PREFERENCES_FONT_BUTTON_H */ diff --git a/src/prefs/dzl-preferences-font-button.ui b/src/prefs/dzl-preferences-font-button.ui new file mode 100644 index 0000000..8ede7b8 --- /dev/null +++ b/src/prefs/dzl-preferences-font-button.ui @@ -0,0 +1,68 @@ + + + + + + box + bottom + 600 + 12 + + + vertical + 12 + true + + + true + + + + + end + _Select + true + true + + + + + + + diff --git a/src/prefs/dzl-preferences-group-private.h b/src/prefs/dzl-preferences-group-private.h new file mode 100644 index 0000000..32d2137 --- /dev/null +++ b/src/prefs/dzl-preferences-group-private.h @@ -0,0 +1,47 @@ +/* dzl-preferences-group-private.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_GROUP_PRIVATE_H +#define DZL_PREFERENCES_GROUP_PRIVATE_H + +#include "prefs/dzl-preferences-group.h" +#include "search/dzl-pattern-spec.h" + +G_BEGIN_DECLS + +struct _DzlPreferencesGroup +{ + GtkBin parent_instance; + + gint priority; + guint is_list : 1; + + GtkLabel *title; + GtkBox *box; + GtkListBox *list_box; + GtkFrame *list_box_frame; + + GPtrArray *widgets; + + GtkListBoxRow *last_focused; + guint last_focused_tab_backward : 1; +}; + +G_END_DECLS + +#endif /* DZL_PREFERENCES_GROUP_PRIVATE_H */ diff --git a/src/prefs/dzl-preferences-group.c b/src/prefs/dzl-preferences-group.c new file mode 100644 index 0000000..c5e13dc --- /dev/null +++ b/src/prefs/dzl-preferences-group.c @@ -0,0 +1,447 @@ +/* dzl-preferences-group.c + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-preferences-group" + +#include "config.h" + +#include "prefs/dzl-preferences-bin.h" +#include "prefs/dzl-preferences-bin-private.h" +#include "prefs/dzl-preferences-entry.h" +#include "prefs/dzl-preferences-group.h" +#include "prefs/dzl-preferences-group-private.h" + +G_DEFINE_TYPE (DzlPreferencesGroup, dzl_preferences_group, GTK_TYPE_BIN) + +#define COLUMN_WIDTH 500 + +enum { + PROP_0, + PROP_IS_LIST, + PROP_MODE, + PROP_PRIORITY, + PROP_TITLE, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +gint +dzl_preferences_group_get_priority (DzlPreferencesGroup *self) +{ + g_return_val_if_fail (DZL_IS_PREFERENCES_GROUP (self), 0); + + return self->priority; +} + +static void +dzl_preferences_group_widget_destroy (DzlPreferencesGroup *self, + GtkWidget *widget) +{ + g_assert (DZL_IS_PREFERENCES_GROUP (self)); + g_assert (GTK_IS_WIDGET (widget)); + + g_ptr_array_remove (self->widgets, widget); +} + +static void +dzl_preferences_group_row_activated (DzlPreferencesGroup *self, + GtkListBoxRow *row, + GtkListBox *list_box) +{ + GtkWidget *child; + + g_assert (DZL_IS_PREFERENCES_GROUP (self)); + g_assert (GTK_IS_LIST_BOX_ROW (row)); + g_assert (GTK_IS_LIST_BOX (list_box)); + + child = gtk_bin_get_child (GTK_BIN (row)); + if (child != NULL) + gtk_widget_activate (child); +} + +static void +dzl_preferences_group_row_selected (DzlPreferencesGroup *self, + GtkListBoxRow *row, + GtkListBox *list_box) +{ + g_assert (DZL_IS_PREFERENCES_GROUP (self)); + g_assert (!row || GTK_IS_LIST_BOX_ROW (row)); + g_assert (GTK_IS_LIST_BOX (list_box)); + + if (gtk_list_box_get_selection_mode (list_box) == GTK_SELECTION_SINGLE && + GTK_IS_LIST_BOX_ROW (row) && + gtk_list_box_row_get_activatable (row)) + dzl_preferences_group_row_activated (self, row, list_box); +} + +const gchar * +dzl_preferences_group_get_title (DzlPreferencesGroup *self) +{ + const gchar *title; + + g_return_val_if_fail (DZL_IS_PREFERENCES_GROUP (self), NULL); + + title = gtk_label_get_label (self->title); + + return (!title || !*title) ? NULL : title; +} + +static void +dzl_preferences_group_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + *min_width = *nat_width = COLUMN_WIDTH; +} + +static void +dzl_preferences_group_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *min_height, + gint *nat_height) +{ + g_assert (GTK_IS_WIDGET (widget)); + g_assert (min_height != NULL); + g_assert (nat_height != NULL); + + GTK_WIDGET_CLASS (dzl_preferences_group_parent_class)->get_preferred_height_for_width (widget, width, min_height, nat_height); +} + +static GtkSizeRequestMode +dzl_preferences_group_get_request_mode (GtkWidget *widget) +{ + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + +static void +dzl_preferences_group_finalize (GObject *object) +{ + DzlPreferencesGroup *self = (DzlPreferencesGroup *)object; + + g_clear_pointer (&self->widgets, g_ptr_array_unref); + + G_OBJECT_CLASS (dzl_preferences_group_parent_class)->finalize (object); +} + +static void +dzl_preferences_group_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesGroup *self = DZL_PREFERENCES_GROUP (object); + + switch (prop_id) + { + case PROP_MODE: + g_value_set_enum (value, gtk_list_box_get_selection_mode (self->list_box)); + break; + + case PROP_IS_LIST: + g_value_set_boolean (value, self->is_list); + break; + + case PROP_PRIORITY: + g_value_set_int (value, self->priority); + break; + + case PROP_TITLE: + g_value_set_string (value, gtk_label_get_label (self->title)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_group_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesGroup *self = DZL_PREFERENCES_GROUP (object); + + switch (prop_id) + { + case PROP_MODE: + gtk_list_box_set_selection_mode (self->list_box, g_value_get_enum (value)); + break; + + case PROP_IS_LIST: + self->is_list = g_value_get_boolean (value); + gtk_widget_set_visible (GTK_WIDGET (self->box), !self->is_list); + gtk_widget_set_visible (GTK_WIDGET (self->list_box_frame), self->is_list); + break; + + case PROP_PRIORITY: + self->priority = g_value_get_int (value); + break; + + case PROP_TITLE: + gtk_label_set_label (self->title, g_value_get_string (value)); + gtk_widget_set_visible (GTK_WIDGET (self->title), !!g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_group_class_init (DzlPreferencesGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_preferences_group_finalize; + object_class->get_property = dzl_preferences_group_get_property; + object_class->set_property = dzl_preferences_group_set_property; + + widget_class->get_preferred_width = dzl_preferences_group_get_preferred_width; + widget_class->get_preferred_height_for_width = dzl_preferences_group_get_preferred_height_for_width; + widget_class->get_request_mode = dzl_preferences_group_get_request_mode; + + properties [PROP_MODE] = + g_param_spec_enum ("mode", + NULL, + NULL, + GTK_TYPE_SELECTION_MODE, + GTK_SELECTION_NONE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_IS_LIST] = + g_param_spec_boolean ("is-list", + "Is List", + "If the group should be rendered as a listbox.", + FALSE, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_PRIORITY] = + g_param_spec_int ("priority", + "Priority", + "Priority", + G_MININT, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "Title", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-preferences-group.ui"); + gtk_widget_class_set_css_name (widget_class, "dzlpreferencesgroup"); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesGroup, box); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesGroup, list_box); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesGroup, list_box_frame); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesGroup, title); +} + +static void +dzl_preferences_group_init (DzlPreferencesGroup *self) +{ + self->widgets = g_ptr_array_new (); + + gtk_widget_init_template (GTK_WIDGET (self)); + + g_signal_connect_object (self->list_box, + "row-activated", + G_CALLBACK (dzl_preferences_group_row_activated), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (self->list_box, + "row-selected", + G_CALLBACK (dzl_preferences_group_row_selected), + self, + G_CONNECT_SWAPPED); +} + +static gboolean +dzl_preferences_group_row_focus (DzlPreferencesGroup *self, + GtkDirectionType dir, + GtkListBoxRow *row) +{ + GtkWidget *child; + GtkWidget *entry; + + self->last_focused_tab_backward = (dir == GTK_DIR_TAB_BACKWARD); + + child = gtk_bin_get_child (GTK_BIN (row)); + + if (DZL_IS_PREFERENCES_ENTRY (child)) + { + entry = dzl_preferences_entry_get_entry_widget ( DZL_PREFERENCES_ENTRY (child)); + if (GTK_IS_ENTRY (entry) && + gtk_widget_is_focus (entry) && + dir == GTK_DIR_TAB_BACKWARD) + gtk_widget_grab_focus (GTK_WIDGET (row)); + } + + return GDK_EVENT_PROPAGATE; +} + +static void +dzl_preferences_group_row_grab_focus (DzlPreferencesGroup *self, + GtkListBoxRow *row) +{ + GtkWidget *child; + GtkListBoxRow *last_focused; + + last_focused = self->last_focused; + child = gtk_bin_get_child (GTK_BIN (row)); + if (DZL_IS_PREFERENCES_ENTRY (child)) + { + self->last_focused = row; + if (row != last_focused || !self->last_focused_tab_backward) + gtk_widget_activate (child); + + return; + } + + self->last_focused = NULL; +} + +void +dzl_preferences_group_add (DzlPreferencesGroup *self, + GtkWidget *widget) +{ + gint position = -1; + + g_return_if_fail (DZL_IS_PREFERENCES_GROUP (self)); + g_return_if_fail (DZL_IS_PREFERENCES_BIN (widget)); + + g_ptr_array_add (self->widgets, widget); + + g_signal_connect_object (widget, + "destroy", + G_CALLBACK (dzl_preferences_group_widget_destroy), + self, + G_CONNECT_SWAPPED); + + if (self->is_list) + { + GtkWidget *row; + + if (GTK_IS_LIST_BOX_ROW (widget)) + row = widget; + else + row = g_object_new (GTK_TYPE_LIST_BOX_ROW, + "child", widget, + "visible", TRUE, + NULL); + + gtk_container_add (GTK_CONTAINER (self->list_box), row); + g_signal_connect_object (row, + "focus", + G_CALLBACK (dzl_preferences_group_row_focus), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (row, + "grab-focus", + G_CALLBACK (dzl_preferences_group_row_grab_focus), + self, + G_CONNECT_AFTER | G_CONNECT_SWAPPED); + } + else + { + gtk_container_add_with_properties (GTK_CONTAINER (self->box), widget, + "position", position, + NULL); + } +} + +void +dzl_preferences_group_set_map (DzlPreferencesGroup *self, + GHashTable *map) +{ + guint i; + + g_return_if_fail (DZL_IS_PREFERENCES_GROUP (self)); + + for (i = 0; i < self->widgets->len; i++) + { + GtkWidget *widget = g_ptr_array_index (self->widgets, i); + + if (DZL_IS_PREFERENCES_BIN (widget)) + _dzl_preferences_bin_set_map (DZL_PREFERENCES_BIN (widget), map); + } +} + +static void +dzl_preferences_group_refilter_cb (GtkWidget *widget, + gpointer user_data) +{ + DzlPreferencesBin *bin = NULL; + struct { + DzlPatternSpec *spec; + guint matches; + } *lookup = user_data; + gboolean matches; + + if (DZL_IS_PREFERENCES_BIN (widget)) + bin = DZL_PREFERENCES_BIN (widget); + else if (GTK_IS_BIN (widget) && DZL_IS_PREFERENCES_BIN (gtk_bin_get_child (GTK_BIN (widget)))) + bin = DZL_PREFERENCES_BIN (gtk_bin_get_child (GTK_BIN (widget))); + else + return; + + if (lookup->spec == NULL) + matches = TRUE; + else + matches = _dzl_preferences_bin_matches (bin, lookup->spec); + + gtk_widget_set_visible (widget, matches); + + lookup->matches += matches; +} + +guint +dzl_preferences_group_refilter (DzlPreferencesGroup *self, + DzlPatternSpec *spec) +{ + struct { + DzlPatternSpec *spec; + guint matches; + } lookup = { spec, 0 }; + const gchar *tmp; + + g_return_val_if_fail (DZL_IS_PREFERENCES_GROUP (self), 0); + + tmp = gtk_label_get_label (self->title); + if (spec && tmp && dzl_pattern_spec_match (spec, tmp)) + lookup.spec = NULL; + + gtk_container_foreach (GTK_CONTAINER (self->list_box), + dzl_preferences_group_refilter_cb, + &lookup); + gtk_container_foreach (GTK_CONTAINER (self->box), + dzl_preferences_group_refilter_cb, + &lookup); + + gtk_widget_set_visible (GTK_WIDGET (self), lookup.matches > 0); + + return lookup.matches; +} diff --git a/src/prefs/dzl-preferences-group.h b/src/prefs/dzl-preferences-group.h new file mode 100644 index 0000000..e8fe9ce --- /dev/null +++ b/src/prefs/dzl-preferences-group.h @@ -0,0 +1,52 @@ +/* dzl-preferences-group.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_GROUP_H +#define DZL_PREFERENCES_GROUP_H + +#include + +#include "dzl-version-macros.h" + +#include "search/dzl-pattern-spec.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PREFERENCES_GROUP (dzl_preferences_group_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlPreferencesGroup, dzl_preferences_group, DZL, PREFERENCES_GROUP, GtkBin) + +DZL_AVAILABLE_IN_ALL +void dzl_preferences_group_add (DzlPreferencesGroup *self, + GtkWidget *widget); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_preferences_group_get_title (DzlPreferencesGroup *self); +DZL_AVAILABLE_IN_ALL +gint dzl_preferences_group_get_priority (DzlPreferencesGroup *self); +DZL_AVAILABLE_IN_ALL +void dzl_preferences_group_set_map (DzlPreferencesGroup *self, + GHashTable *map); +DZL_AVAILABLE_IN_ALL +guint dzl_preferences_group_refilter (DzlPreferencesGroup *self, + DzlPatternSpec *spec); + + +G_END_DECLS + +#endif /* DZL_PREFERENCES_GROUP_H */ diff --git a/src/prefs/dzl-preferences-group.ui b/src/prefs/dzl-preferences-group.ui new file mode 100644 index 0000000..5c81526 --- /dev/null +++ b/src/prefs/dzl-preferences-group.ui @@ -0,0 +1,56 @@ + + + + + diff --git a/src/prefs/dzl-preferences-page-private.h b/src/prefs/dzl-preferences-page-private.h new file mode 100644 index 0000000..3866cc7 --- /dev/null +++ b/src/prefs/dzl-preferences-page-private.h @@ -0,0 +1,37 @@ +/* dzl-preferences-page-private.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_PAGE_PRIVATE_H +#define DZL_PREFERENCES_PAGE_PRIVATE_H + +#include "prefs/dzl-preferences-page.h" +#include "prefs/dzl-preferences-flow-box.h" + +G_BEGIN_DECLS + +struct _DzlPreferencesPage +{ + GtkBin parent_instance; + gint priority; + DzlPreferencesFlowBox *box; + GHashTable *groups_by_name; +}; + +G_END_DECLS + +#endif /* DZL_PREFERENCES_PAGE_PRIVATE_H */ diff --git a/src/prefs/dzl-preferences-page.c b/src/prefs/dzl-preferences-page.c new file mode 100644 index 0000000..43b3873 --- /dev/null +++ b/src/prefs/dzl-preferences-page.c @@ -0,0 +1,187 @@ +/* dzl-preferences-page.c + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#include "config.h" + +#include + +#include "prefs/dzl-preferences-group.h" +#include "prefs/dzl-preferences-group-private.h" +#include "prefs/dzl-preferences-page.h" +#include "prefs/dzl-preferences-page-private.h" + +enum { + PROP_0, + PROP_PRIORITY, + LAST_PROP +}; + +G_DEFINE_TYPE (DzlPreferencesPage, dzl_preferences_page, GTK_TYPE_BIN) + +static GParamSpec *properties [LAST_PROP]; + +static void +dzl_preferences_page_finalize (GObject *object) +{ + DzlPreferencesPage *self = (DzlPreferencesPage *)object; + + g_clear_pointer (&self->groups_by_name, g_hash_table_unref); + + G_OBJECT_CLASS (dzl_preferences_page_parent_class)->finalize (object); +} + +static void +dzl_preferences_page_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesPage *self = DZL_PREFERENCES_PAGE (object); + + switch (prop_id) + { + case PROP_PRIORITY: + g_value_set_int (value, self->priority); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_page_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesPage *self = DZL_PREFERENCES_PAGE (object); + + switch (prop_id) + { + case PROP_PRIORITY: + self->priority = g_value_get_int (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_page_class_init (DzlPreferencesPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_preferences_page_finalize; + object_class->get_property = dzl_preferences_page_get_property; + object_class->set_property = dzl_preferences_page_set_property; + + properties [PROP_PRIORITY] = + g_param_spec_int ("priority", + "Priority", + "Priority", + G_MININT, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-preferences-page.ui"); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesPage, box); +} + +static void +dzl_preferences_page_init (DzlPreferencesPage *self) +{ + self->groups_by_name = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + gtk_widget_init_template (GTK_WIDGET (self)); +} + +void +dzl_preferences_page_add_group (DzlPreferencesPage *self, + DzlPreferencesGroup *group) +{ + gchar *name = NULL; + + g_return_if_fail (DZL_IS_PREFERENCES_PAGE (self)); + g_return_if_fail (DZL_IS_PREFERENCES_GROUP (group)); + + g_object_get (group, "name", &name, NULL); + + if (g_hash_table_contains (self->groups_by_name, name)) + { + g_free (name); + return; + } + + g_hash_table_insert (self->groups_by_name, name, group); + + gtk_container_add_with_properties (GTK_CONTAINER (self->box), GTK_WIDGET (group), + "priority", dzl_preferences_group_get_priority (group), + NULL); +} + +/** + * dzl_preferences_page_get_group: + * + * Returns: (transfer none) (nullable): An #DzlPreferencesGroup or %NULL. + */ +DzlPreferencesGroup * +dzl_preferences_page_get_group (DzlPreferencesPage *self, + const gchar *name) +{ + g_return_val_if_fail (DZL_IS_PREFERENCES_PAGE (self), NULL); + g_return_val_if_fail (name != NULL, NULL); + + return g_hash_table_lookup (self->groups_by_name, name); +} + +void +dzl_preferences_page_set_map (DzlPreferencesPage *self, + GHashTable *map) +{ + DzlPreferencesGroup *group; + GHashTableIter iter; + + g_return_if_fail (DZL_IS_PREFERENCES_PAGE (self)); + + g_hash_table_iter_init (&iter, self->groups_by_name); + + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&group)) + dzl_preferences_group_set_map (group, map); +} + +void +dzl_preferences_page_refilter (DzlPreferencesPage *self, + DzlPatternSpec *spec) +{ + DzlPreferencesGroup *group; + GHashTableIter iter; + guint count = 0; + + g_return_if_fail (DZL_IS_PREFERENCES_PAGE (self)); + + g_hash_table_iter_init (&iter, self->groups_by_name); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&group)) + count += dzl_preferences_group_refilter (group, spec); + gtk_widget_set_visible (GTK_WIDGET (self), count > 0); +} diff --git a/src/prefs/dzl-preferences-page.h b/src/prefs/dzl-preferences-page.h new file mode 100644 index 0000000..60f92ca --- /dev/null +++ b/src/prefs/dzl-preferences-page.h @@ -0,0 +1,51 @@ +/* dzl-preferences-page.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_PAGE_H +#define DZL_PREFERENCES_PAGE_H + +#include + +#include "dzl-version-macros.h" + +#include "prefs/dzl-preferences-group.h" +#include "search/dzl-pattern-spec.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PREFERENCES_PAGE (dzl_preferences_page_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlPreferencesPage, dzl_preferences_page, DZL, PREFERENCES_PAGE, GtkBin) + +DZL_AVAILABLE_IN_ALL +void dzl_preferences_page_add_group (DzlPreferencesPage *self, + DzlPreferencesGroup *group); +DZL_AVAILABLE_IN_ALL +DzlPreferencesGroup *dzl_preferences_page_get_group (DzlPreferencesPage *self, + const gchar *group_name); +DZL_AVAILABLE_IN_ALL +void dzl_preferences_page_refilter (DzlPreferencesPage *self, + DzlPatternSpec *spec); +DZL_AVAILABLE_IN_ALL +void dzl_preferences_page_set_map (DzlPreferencesPage *self, + GHashTable *map); + +G_END_DECLS + +#endif /* DZL_PREFERENCES_PAGE_H */ diff --git a/src/prefs/dzl-preferences-page.ui b/src/prefs/dzl-preferences-page.ui new file mode 100644 index 0000000..8f3f2eb --- /dev/null +++ b/src/prefs/dzl-preferences-page.ui @@ -0,0 +1,11 @@ + + + + + diff --git a/src/prefs/dzl-preferences-spin-button.c b/src/prefs/dzl-preferences-spin-button.c new file mode 100644 index 0000000..8a41b9f --- /dev/null +++ b/src/prefs/dzl-preferences-spin-button.c @@ -0,0 +1,419 @@ +/* dzl-preferences-spin-button.c + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#include "config.h" + +#include "util/dzl-util-private.h" +#include "prefs/dzl-preferences-spin-button.h" + +struct _DzlPreferencesSpinButton +{ + DzlPreferencesBin parent_instance; + + gulong handler; + + guint updating : 1; + + gchar *key; + GSettings *settings; + + const GVariantType *type; + + GtkSpinButton *spin_button; + GtkLabel *title; + GtkLabel *subtitle; +}; + +G_DEFINE_TYPE (DzlPreferencesSpinButton, dzl_preferences_spin_button, DZL_TYPE_PREFERENCES_BIN) + +enum { + PROP_0, + PROP_KEY, + PROP_SUBTITLE, + PROP_TITLE, + LAST_PROP +}; + +enum { + ACTIVATE, + LAST_SIGNAL +}; + +static GParamSpec *properties [LAST_PROP]; +static guint signals [LAST_SIGNAL]; + +static void +dzl_preferences_spin_button_activate (DzlPreferencesSpinButton *self) +{ + g_assert (DZL_IS_PREFERENCES_SPIN_BUTTON (self)); + + gtk_widget_grab_focus (GTK_WIDGET (self->spin_button)); +} + +static void +apply_value (GtkAdjustment *adj, + GVariant *value, + const gchar *property) +{ + GValue val = { 0 }; + gdouble v = 0.0; + + g_assert (GTK_IS_ADJUSTMENT (adj)); + g_assert (value != NULL); + g_assert (property != NULL); + + if (g_variant_is_of_type (value, G_VARIANT_TYPE_DOUBLE)) + v = g_variant_get_double (value); + + else if (g_variant_is_of_type (value, G_VARIANT_TYPE_INT16)) + v = g_variant_get_int16 (value); + else if (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT16)) + v = g_variant_get_uint16 (value); + + else if (g_variant_is_of_type (value, G_VARIANT_TYPE_INT32)) + v = g_variant_get_int32 (value); + else if (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32)) + v = g_variant_get_uint32 (value); + + else if (g_variant_is_of_type (value, G_VARIANT_TYPE_INT64)) + v = g_variant_get_int64 (value); + else if (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT64)) + v = g_variant_get_uint64 (value); + + else + g_warning ("Unknown variant type: %s\n", (gchar *)g_variant_get_type (value)); + + g_value_init (&val, G_TYPE_DOUBLE); + g_value_set_double (&val, v); + g_object_set_property (G_OBJECT (adj), property, &val); + g_value_unset (&val); +} + +static void +dzl_preferences_spin_button_value_changed (DzlPreferencesSpinButton *self, + GParamSpec *pspec, + GtkSpinButton *spin_button) +{ + GVariant *variant = NULL; + gdouble value; + + g_assert (DZL_IS_PREFERENCES_SPIN_BUTTON (self)); + g_assert (pspec != NULL); + g_assert (GTK_IS_SPIN_BUTTON (spin_button)); + + value = gtk_spin_button_get_value (spin_button); + + if (g_variant_type_equal (self->type, G_VARIANT_TYPE_DOUBLE)) + variant = g_variant_new_double (value); + else if (g_variant_type_equal (self->type, G_VARIANT_TYPE_INT16)) + variant = g_variant_new_int16 (value); + else if (g_variant_type_equal (self->type, G_VARIANT_TYPE_UINT16)) + variant = g_variant_new_uint16 (value); + else if (g_variant_type_equal (self->type, G_VARIANT_TYPE_INT32)) + variant = g_variant_new_int32 (value); + else if (g_variant_type_equal (self->type, G_VARIANT_TYPE_UINT32)) + variant = g_variant_new_uint32 (value); + else if (g_variant_type_equal (self->type, G_VARIANT_TYPE_INT64)) + variant = g_variant_new_int64 (value); + else if (g_variant_type_equal (self->type, G_VARIANT_TYPE_UINT64)) + variant = g_variant_new_uint64 (value); + else + g_return_if_reached (); + + g_variant_ref_sink (variant); + g_settings_set_value (self->settings, self->key, variant); + g_clear_pointer (&variant, g_variant_unref); +} + +static void +dzl_preferences_spin_button_setting_changed (DzlPreferencesSpinButton *self, + const gchar *key, + GSettings *settings) +{ + GtkAdjustment *adj; + GVariant *value; + + g_assert (DZL_IS_PREFERENCES_SPIN_BUTTON (self)); + g_assert (key != NULL); + g_assert (G_IS_SETTINGS (settings)); + + if (self->updating) + return; + + self->updating = TRUE; + + adj = gtk_spin_button_get_adjustment (self->spin_button); + + value = g_settings_get_value (settings, key); + apply_value (adj, value, "value"); + g_variant_unref (value); + + self->updating = FALSE; +} + +static void +dzl_preferences_spin_button_connect (DzlPreferencesBin *bin, + GSettings *settings) +{ + DzlPreferencesSpinButton *self = (DzlPreferencesSpinButton *)bin; + GSettingsSchema *schema = NULL; + GSettingsSchemaKey *key = NULL; + GVariant *range = NULL; + GVariant *values = NULL; + GVariant *lower = NULL; + GVariant *upper = NULL; + gchar *type = NULL; + gchar *signal_detail = NULL; + GtkAdjustment *adj; + GVariantIter iter; + + g_assert (DZL_IS_PREFERENCES_SPIN_BUTTON (self)); + + self->settings = g_object_ref (settings); + + g_object_get (self->settings, "settings-schema", &schema, NULL); + + adj = gtk_spin_button_get_adjustment (self->spin_button); + key = g_settings_schema_get_key (schema, self->key); + range = g_settings_schema_key_get_range (key); + + g_variant_get (range, "(sv)", &type, &values); + + if (!dzl_str_equal0 (type, "range") || (2 != g_variant_iter_init (&iter, values))) + { + gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE); + goto cleanup; + } + + lower = g_variant_iter_next_value (&iter); + upper = g_variant_iter_next_value (&iter); + + self->type = g_variant_get_type (lower); + + apply_value (adj, lower, "lower"); + apply_value (adj, upper, "upper"); + + signal_detail = g_strdup_printf ("changed::%s", self->key); + + self->handler = + g_signal_connect_object (self->settings, + signal_detail, + G_CALLBACK (dzl_preferences_spin_button_setting_changed), + self, + G_CONNECT_SWAPPED); + + dzl_preferences_spin_button_setting_changed (self, self->key, self->settings); + +cleanup: + g_clear_pointer (&key, g_settings_schema_key_unref); + g_clear_pointer (&type, g_free); + g_clear_pointer (&signal_detail, g_free); + g_clear_pointer (&range, g_variant_unref); + g_clear_pointer (&values, g_variant_unref); + g_clear_pointer (&lower, g_variant_unref); + g_clear_pointer (&upper, g_variant_unref); + g_clear_pointer (&schema, g_settings_schema_unref); +} + +static void +dzl_preferences_spin_button_disconnect (DzlPreferencesBin *bin, + GSettings *settings) +{ + DzlPreferencesSpinButton *self = (DzlPreferencesSpinButton *)bin; + + g_assert (DZL_IS_PREFERENCES_SPIN_BUTTON (self)); + + g_signal_handler_disconnect (settings, self->handler); + self->handler = 0; +} + +static gboolean +dzl_preferences_spin_button_matches (DzlPreferencesBin *bin, + DzlPatternSpec *spec) +{ + DzlPreferencesSpinButton *self = (DzlPreferencesSpinButton *)bin; + const gchar *tmp; + + g_assert (DZL_IS_PREFERENCES_SPIN_BUTTON (self)); + g_assert (spec != NULL); + + tmp = gtk_label_get_label (self->title); + if (tmp && dzl_pattern_spec_match (spec, tmp)) + return TRUE; + + tmp = gtk_label_get_label (self->subtitle); + if (tmp && dzl_pattern_spec_match (spec, tmp)) + return TRUE; + + if (self->key && dzl_pattern_spec_match (spec, self->key)) + return TRUE; + + return FALSE; +} + +static void +dzl_preferences_spin_button_finalize (GObject *object) +{ + DzlPreferencesSpinButton *self = (DzlPreferencesSpinButton *)object; + + g_clear_pointer (&self->key, g_free); + g_clear_object (&self->settings); + + G_OBJECT_CLASS (dzl_preferences_spin_button_parent_class)->finalize (object); +} + +static void +dzl_preferences_spin_button_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesSpinButton *self = DZL_PREFERENCES_SPIN_BUTTON (object); + + switch (prop_id) + { + case PROP_KEY: + g_value_set_string (value, self->key); + break; + + case PROP_SUBTITLE: + g_value_set_string (value, gtk_label_get_label (self->subtitle)); + break; + + case PROP_TITLE: + g_value_set_string (value, gtk_label_get_label (self->title)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_spin_button_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesSpinButton *self = DZL_PREFERENCES_SPIN_BUTTON (object); + + switch (prop_id) + { + case PROP_KEY: + self->key = g_value_dup_string (value); + break; + + case PROP_SUBTITLE: + gtk_label_set_label (self->subtitle, g_value_get_string (value)); + break; + + case PROP_TITLE: + gtk_label_set_label (self->title, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_spin_button_class_init (DzlPreferencesSpinButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + DzlPreferencesBinClass *bin_class = DZL_PREFERENCES_BIN_CLASS (klass); + + object_class->finalize = dzl_preferences_spin_button_finalize; + object_class->get_property = dzl_preferences_spin_button_get_property; + object_class->set_property = dzl_preferences_spin_button_set_property; + + bin_class->connect = dzl_preferences_spin_button_connect; + bin_class->disconnect = dzl_preferences_spin_button_disconnect; + bin_class->matches = dzl_preferences_spin_button_matches; + + signals [ACTIVATE] = + g_signal_new_class_handler ("activate", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_CALLBACK (dzl_preferences_spin_button_activate), + NULL, NULL, NULL, G_TYPE_NONE, 0); + + widget_class->activate_signal = signals [ACTIVATE]; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-preferences-spin-button.ui"); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSpinButton, spin_button); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSpinButton, subtitle); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSpinButton, title); + + properties [PROP_KEY] = + g_param_spec_string ("key", + "Key", + "Key", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SUBTITLE] = + g_param_spec_string ("subtitle", + "subtitle", + "subtitle", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "title", + "title", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_preferences_spin_button_init (DzlPreferencesSpinButton *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + g_object_set (gtk_spin_button_get_adjustment (self->spin_button), + "value", 0.0, + "lower", 0.0, + "upper", 0.0, + "step-increment", 1.0, + "page-increment", 10.0, + "page-size", 10.0, + NULL); + + g_signal_connect_object (self->spin_button, + "notify::value", + G_CALLBACK (dzl_preferences_spin_button_value_changed), + self, + G_CONNECT_SWAPPED); +} + +/** + * dzl_preferences_spin_button_get_spin_button: + * + * Returns: (transfer none): The actual spin button widget. + */ +GtkWidget * +dzl_preferences_spin_button_get_spin_button (DzlPreferencesSpinButton *self) +{ + g_return_val_if_fail (DZL_IS_PREFERENCES_SPIN_BUTTON (self), NULL); + + return GTK_WIDGET (self->spin_button); +} diff --git a/src/prefs/dzl-preferences-spin-button.h b/src/prefs/dzl-preferences-spin-button.h new file mode 100644 index 0000000..cb45300 --- /dev/null +++ b/src/prefs/dzl-preferences-spin-button.h @@ -0,0 +1,40 @@ +/* dzl-preferences-spin-button.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_SPIN_BUTTON_H +#define DZL_PREFERENCES_SPIN_BUTTON_H + +#include + +#include "dzl-version-macros.h" + +#include "prefs/dzl-preferences-bin.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PREFERENCES_SPIN_BUTTON (dzl_preferences_spin_button_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlPreferencesSpinButton, dzl_preferences_spin_button, DZL, PREFERENCES_SPIN_BUTTON, DzlPreferencesBin) + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_preferences_spin_button_get_spin_button (DzlPreferencesSpinButton *self); + +G_END_DECLS + +#endif /* DZL_PREFERENCES_SPIN_BUTTON_H */ diff --git a/src/prefs/dzl-preferences-spin-button.ui b/src/prefs/dzl-preferences-spin-button.ui new file mode 100644 index 0000000..8b1d92f --- /dev/null +++ b/src/prefs/dzl-preferences-spin-button.ui @@ -0,0 +1,56 @@ + + + + + diff --git a/src/prefs/dzl-preferences-switch.c b/src/prefs/dzl-preferences-switch.c new file mode 100644 index 0000000..d34baf2 --- /dev/null +++ b/src/prefs/dzl-preferences-switch.c @@ -0,0 +1,441 @@ +/* dzl-preferences-switch.c + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#include "config.h" + +#include "util/dzl-util-private.h" +#include "prefs/dzl-preferences-switch.h" + +struct _DzlPreferencesSwitch +{ + DzlPreferencesBin parent_instance; + + guint is_radio : 1; + guint updating : 1; + + gulong handler; + + gchar *key; + GVariant *target; + GSettings *settings; + + GtkLabel *subtitle; + GtkLabel *title; + GtkSwitch *widget; + GtkImage *image; +}; + +G_DEFINE_TYPE (DzlPreferencesSwitch, dzl_preferences_switch, DZL_TYPE_PREFERENCES_BIN) + +enum { + PROP_0, + PROP_IS_RADIO, + PROP_KEY, + PROP_SUBTITLE, + PROP_TARGET, + PROP_TITLE, + LAST_PROP +}; + +enum { + ACTIVATED, + LAST_SIGNAL +}; + +static GParamSpec *properties [LAST_PROP]; +static guint signals [LAST_SIGNAL]; + +static void +dzl_preferences_switch_changed (DzlPreferencesSwitch *self, + const gchar *key, + GSettings *settings) +{ + GVariant *value; + gboolean active = FALSE; + + g_assert (DZL_IS_PREFERENCES_SWITCH (self)); + g_assert (key != NULL); + g_assert (G_IS_SETTINGS (settings)); + + if (self->updating == TRUE) + return; + + value = g_settings_get_value (settings, key); + + if (g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN)) + active = g_variant_get_boolean (value); + else if ((self->target != NULL) && + g_variant_is_of_type (value, g_variant_get_type (self->target))) + active = g_variant_equal (value, self->target); + else if ((self->target != NULL) && + g_variant_is_of_type (self->target, G_VARIANT_TYPE_STRING) && + g_variant_is_of_type (value, G_VARIANT_TYPE_STRING_ARRAY)) + { + g_autofree const gchar **strv = g_variant_get_strv (value, NULL); + const gchar *flag = g_variant_get_string (self->target, NULL); + active = g_strv_contains (strv, flag); + } + + self->updating = TRUE; + + if (self->is_radio) + { + gtk_widget_set_visible (GTK_WIDGET (self->image), active); + } + else + { + gtk_switch_set_active (self->widget, active); + gtk_switch_set_state (self->widget, active); + } + + self->updating = FALSE; + + g_variant_unref (value); +} + +static void +dzl_preferences_switch_connect (DzlPreferencesBin *bin, + GSettings *settings) +{ + DzlPreferencesSwitch *self = (DzlPreferencesSwitch *)bin; + g_autofree gchar *signal_detail = NULL; + + g_assert (DZL_IS_PREFERENCES_SWITCH (self)); + + signal_detail = g_strdup_printf ("changed::%s", self->key); + + self->settings = g_object_ref (settings); + + self->handler = + g_signal_connect_object (settings, + signal_detail, + G_CALLBACK (dzl_preferences_switch_changed), + self, + G_CONNECT_SWAPPED); + + dzl_preferences_switch_changed (self, self->key, settings); +} + +static void +dzl_preferences_switch_disconnect (DzlPreferencesBin *bin, + GSettings *settings) +{ + DzlPreferencesSwitch *self = (DzlPreferencesSwitch *)bin; + + g_assert (DZL_IS_PREFERENCES_SWITCH (self)); + + g_signal_handler_disconnect (settings, self->handler); + self->handler = 0; +} + +static void +dzl_preferences_switch_toggle (DzlPreferencesSwitch *self, + gboolean state) +{ + GVariant *value; + + g_assert (DZL_IS_PREFERENCES_SWITCH (self)); + + if (self->updating) + return; + + self->updating = TRUE; + + value = g_settings_get_value (self->settings, self->key); + + if (g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN)) + { + g_settings_set_boolean (self->settings, self->key, state); + } + else if ((self->target != NULL) && + g_variant_is_of_type (self->target, G_VARIANT_TYPE_STRING) && + g_variant_is_of_type (value, G_VARIANT_TYPE_STRING_ARRAY)) + { + g_autofree const gchar **strv = g_variant_get_strv (value, NULL); + g_autoptr(GPtrArray) ar = g_ptr_array_new (); + const gchar *flag = g_variant_get_string (self->target, NULL); + gboolean found = FALSE; + gint i; + + for (i = 0; strv [i]; i++) + { + if (!state && dzl_str_equal0 (strv [i], flag)) + continue; + if (dzl_str_equal0 (strv [i], flag)) + found = TRUE; + g_ptr_array_add (ar, (gchar *)strv [i]); + } + + if (state && !found) + g_ptr_array_add (ar, (gchar *)flag); + + g_ptr_array_add (ar, NULL); + + g_settings_set_strv (self->settings, self->key, (const gchar * const *)ar->pdata); + } + else if ((self->target != NULL) && + g_variant_is_of_type (value, g_variant_get_type (self->target))) + { + g_settings_set_value (self->settings, self->key, self->target); + } + else + { + g_warning ("I don't know how to set a variant of type %s to %s", + (const gchar *)g_variant_get_type (value), + self->target ? (const gchar *)g_variant_get_type (self->target) : "(nil)"); + } + + + g_variant_unref (value); + + if (self->is_radio) + gtk_widget_set_visible (GTK_WIDGET (self->image), state); + else + gtk_switch_set_state (self->widget, state); + + self->updating = FALSE; + + /* For good measure, so that we cleanup in the boolean deselection case */ + dzl_preferences_switch_changed (self, self->key, self->settings); +} + +static gboolean +dzl_preferences_switch_state_set (DzlPreferencesSwitch *self, + gboolean state, + GtkSwitch *widget) +{ + g_assert (DZL_IS_PREFERENCES_SWITCH (self)); + g_assert (GTK_IS_SWITCH (widget)); + + dzl_preferences_switch_toggle (self, state); + + return TRUE; +} + +static void +dzl_preferences_switch_activate (DzlPreferencesSwitch *self) +{ + g_assert (DZL_IS_PREFERENCES_SWITCH (self)); + + if (!gtk_widget_get_sensitive (GTK_WIDGET (self)) || (self->settings == NULL)) + return; + + if (self->is_radio) + { + gboolean state; + + state = !gtk_widget_get_visible (GTK_WIDGET (self->image)); + dzl_preferences_switch_toggle (self, state); + } + else + gtk_widget_activate (GTK_WIDGET (self->widget)); + +} + +static gboolean +dzl_preferences_switch_matches (DzlPreferencesBin *bin, + DzlPatternSpec *spec) +{ + DzlPreferencesSwitch *self = (DzlPreferencesSwitch *)bin; + const gchar *tmp; + + g_assert (DZL_IS_PREFERENCES_SWITCH (self)); + g_assert (spec != NULL); + + tmp = gtk_label_get_label (self->title); + if (tmp && dzl_pattern_spec_match (spec, tmp)) + return TRUE; + + tmp = gtk_label_get_label (self->subtitle); + if (tmp && dzl_pattern_spec_match (spec, tmp)) + return TRUE; + + if (self->key && dzl_pattern_spec_match (spec, self->key)) + return TRUE; + + return FALSE; +} + +static void +dzl_preferences_switch_finalize (GObject *object) +{ + DzlPreferencesSwitch *self = (DzlPreferencesSwitch *)object; + + g_clear_pointer (&self->key, g_free); + g_clear_pointer (&self->target, g_variant_unref); + g_clear_object (&self->settings); + + G_OBJECT_CLASS (dzl_preferences_switch_parent_class)->finalize (object); +} + +static void +dzl_preferences_switch_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesSwitch *self = DZL_PREFERENCES_SWITCH (object); + + switch (prop_id) + { + case PROP_IS_RADIO: + g_value_set_boolean (value, self->is_radio); + break; + + case PROP_KEY: + g_value_set_string (value, self->key); + break; + + case PROP_TARGET: + g_value_set_variant (value, self->target); + break; + + case PROP_TITLE: + g_value_set_string (value, gtk_label_get_label (self->title)); + break; + + case PROP_SUBTITLE: + g_value_set_string (value, gtk_label_get_label (self->subtitle)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_switch_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPreferencesSwitch *self = DZL_PREFERENCES_SWITCH (object); + + switch (prop_id) + { + case PROP_IS_RADIO: + self->is_radio = g_value_get_boolean (value); + gtk_widget_set_visible (GTK_WIDGET (self->widget), !self->is_radio); + gtk_widget_set_visible (GTK_WIDGET (self->image), self->is_radio); + break; + + case PROP_KEY: + self->key = g_value_dup_string (value); + break; + + case PROP_TARGET: + self->target = g_value_dup_variant (value); + break; + + case PROP_TITLE: + gtk_label_set_label (self->title, g_value_get_string (value)); + break; + + case PROP_SUBTITLE: + g_object_set (self->subtitle, + "label", g_value_get_string (value), + "visible", !!g_value_get_string (value), + NULL); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_preferences_switch_class_init (DzlPreferencesSwitchClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + DzlPreferencesBinClass *bin_class = DZL_PREFERENCES_BIN_CLASS (klass); + + object_class->finalize = dzl_preferences_switch_finalize; + object_class->get_property = dzl_preferences_switch_get_property; + object_class->set_property = dzl_preferences_switch_set_property; + + bin_class->connect = dzl_preferences_switch_connect; + bin_class->disconnect = dzl_preferences_switch_disconnect; + bin_class->matches = dzl_preferences_switch_matches; + + signals [ACTIVATED] = + g_signal_new_class_handler ("activated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_CALLBACK (dzl_preferences_switch_activate), + NULL, NULL, NULL, G_TYPE_NONE, 0); + + widget_class->activate_signal = signals [ACTIVATED]; + + properties [PROP_IS_RADIO] = + g_param_spec_boolean ("is-radio", + "Is Radio", + "If a radio style should be used instead of a switch.", + FALSE, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + + properties [PROP_TARGET] = + g_param_spec_variant ("target", + "Target", + "Target", + G_VARIANT_TYPE_ANY, + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_KEY] = + g_param_spec_string ("key", + "Key", + "Key", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "Title", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SUBTITLE] = + g_param_spec_string ("subtitle", + "Subtitle", + "Subtitle", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-preferences-switch.ui"); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSwitch, image); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSwitch, subtitle); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSwitch, title); + gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSwitch, widget); +} + +static void +dzl_preferences_switch_init (DzlPreferencesSwitch *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + g_signal_connect_object (self->widget, + "state-set", + G_CALLBACK (dzl_preferences_switch_state_set), + self, + G_CONNECT_SWAPPED); +} diff --git a/src/prefs/dzl-preferences-switch.h b/src/prefs/dzl-preferences-switch.h new file mode 100644 index 0000000..732dacc --- /dev/null +++ b/src/prefs/dzl-preferences-switch.h @@ -0,0 +1,35 @@ +/* dzl-preferences-switch.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_SWITCH_H +#define DZL_PREFERENCES_SWITCH_H + +#include "dzl-version-macros.h" + +#include "prefs/dzl-preferences-bin.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PREFERENCES_SWITCH (dzl_preferences_switch_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlPreferencesSwitch, dzl_preferences_switch, DZL, PREFERENCES_SWITCH, DzlPreferencesBin) + +G_END_DECLS + +#endif /* DZL_PREFERENCES_SWITCH_H */ diff --git a/src/prefs/dzl-preferences-switch.ui b/src/prefs/dzl-preferences-switch.ui new file mode 100644 index 0000000..e11b0b0 --- /dev/null +++ b/src/prefs/dzl-preferences-switch.ui @@ -0,0 +1,59 @@ + + + + + diff --git a/src/prefs/dzl-preferences-view.c b/src/prefs/dzl-preferences-view.c new file mode 100644 index 0000000..c7ac160 --- /dev/null +++ b/src/prefs/dzl-preferences-view.c @@ -0,0 +1,961 @@ +/* dzl-preferences-view.c + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-preferences-view" + +#include "config.h" + +#include + +#include "prefs/dzl-preferences-file-chooser-button.h" +#include "prefs/dzl-preferences-font-button.h" +#include "prefs/dzl-preferences-group-private.h" +#include "prefs/dzl-preferences-page-private.h" +#include "prefs/dzl-preferences-spin-button.h" +#include "prefs/dzl-preferences-switch.h" +#include "prefs/dzl-preferences-view.h" +#include "util/dzl-util-private.h" + +typedef struct +{ + GActionGroup *actions; + GSequence *pages; + GHashTable *widgets; + + GtkScrolledWindow *scroller; + GtkStack *page_stack; + GtkStackSwitcher *page_stack_sidebar; + GtkSearchEntry *search_entry; + GtkStack *subpage_stack; + + guint last_widget_id; +} DzlPreferencesViewPrivate; + +typedef struct +{ + GtkWidget *widget; + gulong handler; + guint id; +} TrackedWidget; + +static void dzl_preferences_iface_init (DzlPreferencesInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (DzlPreferencesView, dzl_preferences_view, GTK_TYPE_BIN, + G_ADD_PRIVATE (DzlPreferencesView) + G_IMPLEMENT_INTERFACE (DZL_TYPE_PREFERENCES, dzl_preferences_iface_init)) + +static void +tracked_widget_free (gpointer data) +{ + TrackedWidget *tracked = data; + + if (tracked->widget != NULL) + { + dzl_clear_signal_handler (tracked->widget, &tracked->handler); + tracked->widget = NULL; + } + + tracked->handler = 0; + tracked->id = 0; + + g_slice_free (TrackedWidget, tracked); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (TrackedWidget, tracked_widget_free) + +static void +dzl_preferences_view_track (DzlPreferencesView *self, + guint id, + GtkWidget *widget) +{ + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + TrackedWidget *tracked; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (id > 0); + g_assert (GTK_IS_WIDGET (widget)); + + tracked = g_slice_new0 (TrackedWidget); + tracked->widget = widget; + tracked->id = id; + tracked->handler = g_signal_connect (widget, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &tracked->widget); + + g_hash_table_insert (priv->widgets, GUINT_TO_POINTER (id), tracked); +} + +static void +dzl_preferences_view_refilter_cb (GtkWidget *widget, + gpointer user_data) +{ + DzlPreferencesPage *page = (DzlPreferencesPage *)widget; + DzlPatternSpec *spec = user_data; + + g_assert (DZL_IS_PREFERENCES_PAGE (page)); + + dzl_preferences_page_refilter (page, spec); +} + +static void +dzl_preferences_view_refilter (DzlPreferencesView *self, + const gchar *search_text) +{ + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + DzlPatternSpec *spec = NULL; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + + if (!dzl_str_empty0 (search_text)) + spec = dzl_pattern_spec_new (search_text); + + gtk_container_foreach (GTK_CONTAINER (priv->page_stack), + dzl_preferences_view_refilter_cb, + spec); + gtk_container_foreach (GTK_CONTAINER (priv->subpage_stack), + dzl_preferences_view_refilter_cb, + spec); + + g_clear_pointer (&spec, dzl_pattern_spec_unref); +} + +static gint +sort_by_priority (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + gint prioritya = 0; + gint priorityb = 0; + + g_object_get ((gpointer)a, "priority", &prioritya, NULL); + g_object_get ((gpointer)b, "priority", &priorityb, NULL); + + return prioritya - priorityb; +} + +static void +dzl_preferences_view_notify_visible_child (DzlPreferencesView *self, + GParamSpec *pspec, + GtkStack *stack) +{ + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + DzlPreferencesPage *page; + GHashTableIter iter; + gpointer value; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + + /* Short circuit if we are destroying everything */ + if (gtk_widget_in_destruction (GTK_WIDGET (self))) + return; + + gtk_widget_hide (GTK_WIDGET (priv->subpage_stack)); + + /* + * If there are any selections in list groups, re-select it to cause + * the subpage to potentially reappear. + */ + + if (NULL == (page = DZL_PREFERENCES_PAGE (gtk_stack_get_visible_child (stack)))) + return; + + g_hash_table_iter_init (&iter, page->groups_by_name); + + while (g_hash_table_iter_next (&iter, NULL, &value)) + { + DzlPreferencesGroup *group = value; + GtkSelectionMode mode = GTK_SELECTION_NONE; + + g_assert (DZL_IS_PREFERENCES_GROUP (group)); + + if (!group->is_list) + continue; + + g_object_get (group, "mode", &mode, NULL); + + if (mode == GTK_SELECTION_SINGLE) + { + GtkListBoxRow *selected; + + selected = gtk_list_box_get_selected_row (group->list_box); + + g_assert (!selected || GTK_IS_LIST_BOX_ROW (selected)); + + if (selected != NULL && gtk_widget_activate (GTK_WIDGET (selected))) + break; + } + } +} + +static void +dzl_preferences_view_finalize (GObject *object) +{ + DzlPreferencesView *self = (DzlPreferencesView *)object; + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + + g_clear_pointer (&priv->pages, g_sequence_free); + g_clear_pointer (&priv->widgets, g_hash_table_unref); + g_clear_object (&priv->actions); + + G_OBJECT_CLASS (dzl_preferences_view_parent_class)->finalize (object); +} + +static void +dzl_preferences_view_class_init (DzlPreferencesViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_preferences_view_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-preferences-view.ui"); + gtk_widget_class_set_css_name (widget_class, "dzlpreferencesview"); + gtk_widget_class_bind_template_child_private (widget_class, DzlPreferencesView, page_stack); + gtk_widget_class_bind_template_child_private (widget_class, DzlPreferencesView, page_stack_sidebar); + gtk_widget_class_bind_template_child_private (widget_class, DzlPreferencesView, scroller); + gtk_widget_class_bind_template_child_private (widget_class, DzlPreferencesView, search_entry); + gtk_widget_class_bind_template_child_private (widget_class, DzlPreferencesView, subpage_stack); +} + +static void +go_back_activate (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + DzlPreferencesView *self = user_data; + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + + gtk_widget_hide (GTK_WIDGET (priv->subpage_stack)); +} + +void +dzl_preferences_view_reapply_filter (DzlPreferencesView *self) +{ + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + + g_return_if_fail (DZL_IS_PREFERENCES_VIEW (self)); + + dzl_preferences_view_refilter (self, gtk_entry_get_text (GTK_ENTRY (priv->search_entry))); +} + +static void +dzl_preferences_view_search_entry_changed (DzlPreferencesView *self, + GtkSearchEntry *search_entry) +{ + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (GTK_IS_SEARCH_ENTRY (search_entry)); + + dzl_preferences_view_reapply_filter (self); +} + +static void +dzl_preferences_view_notify_subpage_stack_visible (DzlPreferencesView *self, + GParamSpec *pspec, + GtkStack *subpage_stack) +{ + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (GTK_IS_STACK (subpage_stack)); + + /* + * Because the subpage stack can cause us to have a wider display than + * the screen has, we need to allow scrolling. This can happen because + * side-by-side we could be just a bit bigger than 1280px which is a + * fairly common laptop screen size (especially under HiDPI). + * + * https://bugzilla.gnome.org/show_bug.cgi?id=772700 + */ + + if (gtk_widget_get_visible (GTK_WIDGET (subpage_stack))) + g_object_set (priv->scroller, "hscrollbar-policy", GTK_POLICY_AUTOMATIC, NULL); + else + g_object_set (priv->scroller, "hscrollbar-policy", GTK_POLICY_NEVER, NULL); +} + +static void +dzl_preferences_view_init (DzlPreferencesView *self) +{ + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + static const GActionEntry entries[] = { + { "go-back", go_back_activate }, + }; + + gtk_widget_init_template (GTK_WIDGET (self)); + + g_signal_connect_object (priv->search_entry, + "changed", + G_CALLBACK (dzl_preferences_view_search_entry_changed), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (priv->page_stack, + "notify::visible-child", + G_CALLBACK (dzl_preferences_view_notify_visible_child), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (priv->subpage_stack, + "notify::visible", + G_CALLBACK (dzl_preferences_view_notify_subpage_stack_visible), + self, + G_CONNECT_SWAPPED); + + priv->pages = g_sequence_new (NULL); + priv->widgets = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, tracked_widget_free); + + priv->actions = G_ACTION_GROUP (g_simple_action_group_new ()); + g_action_map_add_action_entries (G_ACTION_MAP (priv->actions), + entries, G_N_ELEMENTS (entries), + self); +} + +static GtkWidget * +dzl_preferences_view_get_page (DzlPreferencesView *self, + const gchar *page_name) +{ + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (page_name != NULL); + + if (strchr (page_name, '.') != NULL) + return gtk_stack_get_child_by_name (priv->subpage_stack, page_name); + else + return gtk_stack_get_child_by_name (priv->page_stack, page_name); +} + +static void +dzl_preferences_view_add_page (DzlPreferences *preferences, + const gchar *page_name, + const gchar *title, + gint priority) +{ + DzlPreferencesView *self = (DzlPreferencesView *)preferences; + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + DzlPreferencesPage *page; + GSequenceIter *iter; + GtkStack *stack; + gint position = -1; + + g_assert (DZL_IS_PREFERENCES (preferences)); + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (page_name != NULL); + g_assert (title != NULL || strchr (page_name, '.')); + + if (strchr (page_name, '.') != NULL) + stack = priv->subpage_stack; + else + stack = priv->page_stack; + + if (gtk_stack_get_child_by_name (stack, page_name)) + return; + + page = g_object_new (DZL_TYPE_PREFERENCES_PAGE, + "priority", priority, + "visible", TRUE, + NULL); + + if (stack == priv->page_stack) + { + iter = g_sequence_insert_sorted (priv->pages, page, sort_by_priority, NULL); + position = g_sequence_iter_get_position (iter); + } + + gtk_container_add_with_properties (GTK_CONTAINER (stack), GTK_WIDGET (page), + "position", position, + "name", page_name, + "title", title, + NULL); +} + +static void +dzl_preferences_view_add_group (DzlPreferences *preferences, + const gchar *page_name, + const gchar *group_name, + const gchar *title, + gint priority) +{ + DzlPreferencesView *self = (DzlPreferencesView *)preferences; + DzlPreferencesGroup *group; + GtkWidget *page; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (page_name != NULL); + g_assert (group_name != NULL); + + page = dzl_preferences_view_get_page (self, page_name); + + if (page == NULL) + { + g_warning ("No page named \"%s\" could be found.", page_name); + return; + } + + group = g_object_new (DZL_TYPE_PREFERENCES_GROUP, + "name", group_name, + "priority", priority, + "title", title, + "visible", TRUE, + NULL); + dzl_preferences_page_add_group (DZL_PREFERENCES_PAGE (page), group); +} + +static void +dzl_preferences_view_add_list_group (DzlPreferences *preferences, + const gchar *page_name, + const gchar *group_name, + const gchar *title, + GtkSelectionMode mode, + gint priority) +{ + DzlPreferencesView *self = (DzlPreferencesView *)preferences; + DzlPreferencesGroup *group; + GtkWidget *page; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (page_name != NULL); + g_assert (group_name != NULL); + + page = dzl_preferences_view_get_page (self, page_name); + + if (page == NULL) + { + g_warning ("No page named \"%s\" could be found.", page_name); + return; + } + + group = g_object_new (DZL_TYPE_PREFERENCES_GROUP, + "is-list", TRUE, + "mode", mode, + "name", group_name, + "priority", priority, + "title", title, + "visible", TRUE, + NULL); + dzl_preferences_page_add_group (DZL_PREFERENCES_PAGE (page), group); +} + +static guint +dzl_preferences_view_add_radio (DzlPreferences *preferences, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *variant_string, + const gchar *title, + const gchar *subtitle, + const gchar *keywords, + gint priority) +{ + DzlPreferencesView *self = (DzlPreferencesView *)preferences; + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + DzlPreferencesSwitch *widget; + DzlPreferencesGroup *group; + g_autoptr(GVariant) variant = NULL; + GtkWidget *page; + guint widget_id; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (page_name != NULL); + g_assert (group_name != NULL); + g_assert (schema_id != NULL); + g_assert (key != NULL); + g_assert (title != NULL); + + page = dzl_preferences_view_get_page (self, page_name); + + if (page == NULL) + { + g_warning ("No page named \"%s\" could be found.", page_name); + return 0; + } + + group = dzl_preferences_page_get_group (DZL_PREFERENCES_PAGE (page), group_name); + + if (group == NULL) + { + g_warning ("No such preferences group \"%s\" in page \"%s\"", + group_name, page_name); + return 0; + } + + if (variant_string != NULL) + { + g_autoptr(GError) error = NULL; + + variant = g_variant_parse (NULL, variant_string, NULL, NULL, &error); + + if (variant == NULL) + g_warning ("%s", error->message); + } + + widget = g_object_new (DZL_TYPE_PREFERENCES_SWITCH, + "is-radio", TRUE, + "key", key, + "keywords", keywords, + "path", path, + "priority", priority, + "schema-id", schema_id, + "subtitle", subtitle, + "target", variant, + "title", title, + "visible", TRUE, + NULL); + + dzl_preferences_group_add (group, GTK_WIDGET (widget)); + + widget_id = ++priv->last_widget_id; + dzl_preferences_view_track (self, widget_id, GTK_WIDGET (widget)); + + return widget_id; +} + +static guint +dzl_preferences_view_add_switch (DzlPreferences *preferences, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *variant_string, + const gchar *title, + const gchar *subtitle, + const gchar *keywords, + gint priority) +{ + DzlPreferencesView *self = (DzlPreferencesView *)preferences; + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + DzlPreferencesSwitch *widget; + DzlPreferencesGroup *group; + g_autoptr(GVariant) variant = NULL; + GtkWidget *page; + guint widget_id; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (page_name != NULL); + g_assert (group_name != NULL); + g_assert (schema_id != NULL); + g_assert (key != NULL); + g_assert (title != NULL); + + page = dzl_preferences_view_get_page (self, page_name); + + if (page == NULL) + { + g_warning ("No page named \"%s\" could be found.", page_name); + return 0; + } + + group = dzl_preferences_page_get_group (DZL_PREFERENCES_PAGE (page), group_name); + + if (group == NULL) + { + g_warning ("No such preferences group \"%s\" in page \"%s\"", + group_name, page_name); + return 0; + } + + if (variant_string != NULL) + { + g_autoptr(GError) error = NULL; + + variant = g_variant_parse (NULL, variant_string, NULL, NULL, &error); + + if (variant == NULL) + g_warning ("%s", error->message); + } + + widget = g_object_new (DZL_TYPE_PREFERENCES_SWITCH, + "key", key, + "keywords", keywords, + "path", path, + "priority", priority, + "schema-id", schema_id, + "subtitle", subtitle, + "target", variant, + "title", title, + "visible", TRUE, + NULL); + + dzl_preferences_group_add (group, GTK_WIDGET (widget)); + + widget_id = ++priv->last_widget_id; + dzl_preferences_view_track (self, widget_id, GTK_WIDGET (widget)); + + return widget_id; +} + +static guint +dzl_preferences_view_add_spin_button (DzlPreferences *preferences, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *title, + const gchar *subtitle, + const gchar *keywords, + gint priority) +{ + DzlPreferencesView *self = (DzlPreferencesView *)preferences; + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + DzlPreferencesSpinButton *widget; + DzlPreferencesGroup *group; + GtkWidget *page; + guint widget_id; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (page_name != NULL); + g_assert (group_name != NULL); + g_assert (schema_id != NULL); + g_assert (key != NULL); + g_assert (title != NULL); + + page = dzl_preferences_view_get_page (self, page_name); + + if (page == NULL) + { + g_warning ("No page named \"%s\" could be found.", page_name); + return 0; + } + + group = dzl_preferences_page_get_group (DZL_PREFERENCES_PAGE (page), group_name); + + + if (group == NULL) + { + g_warning ("No such preferences group \"%s\" in page \"%s\"", + group_name, page_name); + return 0; + } + + widget = g_object_new (DZL_TYPE_PREFERENCES_SPIN_BUTTON, + "key", key, + "keywords", keywords, + "path", path, + "priority", priority, + "schema-id", schema_id, + "subtitle", subtitle, + "title", title, + "visible", TRUE, + NULL); + + dzl_preferences_group_add (group, GTK_WIDGET (widget)); + + widget_id = ++priv->last_widget_id; + dzl_preferences_view_track (self, widget_id, GTK_WIDGET (widget)); + + return widget_id; +} + +static guint +dzl_preferences_view_add_font_button (DzlPreferences *preferences, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *title, + const gchar *keywords, + gint priority) +{ + DzlPreferencesView *self = (DzlPreferencesView *)preferences; + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + DzlPreferencesSwitch *widget; + DzlPreferencesGroup *group; + GtkWidget *page; + guint widget_id; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (page_name != NULL); + g_assert (group_name != NULL); + g_assert (schema_id != NULL); + g_assert (key != NULL); + g_assert (title != NULL); + + page = dzl_preferences_view_get_page (self, page_name); + + if (page == NULL) + { + g_warning ("No page named \"%s\" could be found.", page_name); + return 0; + } + + group = dzl_preferences_page_get_group (DZL_PREFERENCES_PAGE (page), group_name); + + if (group == NULL) + { + g_warning ("No such preferences group \"%s\" in page \"%s\"", + group_name, page_name); + return 0; + } + + widget = g_object_new (DZL_TYPE_PREFERENCES_FONT_BUTTON, + "key", key, + "keywords", keywords, + "priority", priority, + "schema-id", schema_id, + "title", title, + "visible", TRUE, + NULL); + + dzl_preferences_group_add (group, GTK_WIDGET (widget)); + + widget_id = ++priv->last_widget_id; + dzl_preferences_view_track (self, widget_id, GTK_WIDGET (widget)); + + return widget_id; +} + +static guint +dzl_preferences_view_add_file_chooser (DzlPreferences *preferences, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *title, + const gchar *subtitle, + GtkFileChooserAction action, + const gchar *keywords, + gint priority) +{ + DzlPreferencesView *self = (DzlPreferencesView *)preferences; + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + DzlPreferencesFileChooserButton *widget; + DzlPreferencesGroup *group; + GtkWidget *page; + guint widget_id; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (page_name != NULL); + g_assert (group_name != NULL); + g_assert (schema_id != NULL); + g_assert (key != NULL); + g_assert (title != NULL); + + page = dzl_preferences_view_get_page (self, page_name); + + if (page == NULL) + { + g_warning ("No page named \"%s\" could be found.", page_name); + return 0; + } + + group = dzl_preferences_page_get_group (DZL_PREFERENCES_PAGE (page), group_name); + + if (group == NULL) + { + g_warning ("No such preferences group \"%s\" in page \"%s\"", + group_name, page_name); + return 0; + } + + widget = g_object_new (DZL_TYPE_PREFERENCES_FILE_CHOOSER_BUTTON, + "action", action, + "key", key, + "priority", priority, + "schema-id", schema_id, + "path", path, + "subtitle", subtitle, + "title", title, + "keywords", keywords, + "visible", TRUE, + NULL); + + dzl_preferences_group_add (group, GTK_WIDGET (widget)); + + widget_id = ++priv->last_widget_id; + dzl_preferences_view_track (self, widget_id, GTK_WIDGET (widget)); + + return widget_id; +} + +static guint +dzl_preferences_view_add_custom (DzlPreferences *preferences, + const gchar *page_name, + const gchar *group_name, + GtkWidget *widget, + const gchar *keywords, + gint priority) +{ + DzlPreferencesView *self = (DzlPreferencesView *)preferences; + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + DzlPreferencesBin *container; + DzlPreferencesGroup *group; + GtkWidget *page; + guint widget_id; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (page_name != NULL); + g_assert (group_name != NULL); + g_assert (GTK_IS_WIDGET (widget)); + + page = dzl_preferences_view_get_page (self, page_name); + + if (page == NULL) + { + g_warning ("No page named \"%s\" could be found.", page_name); + return 0; + } + + group = dzl_preferences_page_get_group (DZL_PREFERENCES_PAGE (page), group_name); + + if (group == NULL) + { + g_warning ("No such preferences group \"%s\" in page \"%s\"", + group_name, page_name); + return 0; + } + + widget_id = ++priv->last_widget_id; + + gtk_widget_show (widget); + gtk_widget_show (GTK_WIDGET (group)); + + if (DZL_IS_PREFERENCES_BIN (widget)) + container = DZL_PREFERENCES_BIN (widget); + else + container = g_object_new (DZL_TYPE_PREFERENCES_BIN, + "child", widget, + "keywords", keywords, + "priority", priority, + "visible", TRUE, + NULL); + + dzl_preferences_group_add (group, GTK_WIDGET (container)); + + dzl_preferences_view_track (self, widget_id, GTK_WIDGET (widget)); + + return widget_id; +} + +static gboolean +dzl_preferences_view_remove_id (DzlPreferences *preferences, + guint widget_id) +{ + DzlPreferencesView *self = (DzlPreferencesView *)preferences; + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + g_autoptr(TrackedWidget) tracked = NULL; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (widget_id != 0); + + tracked = g_hash_table_lookup (priv->widgets, GUINT_TO_POINTER (widget_id)); + + if (tracked != NULL) + { + GtkWidget *widget = tracked->widget; + + /* We have to steal the structure so that we retain access to + * the structure after removing it from the hashtable. + */ + g_hash_table_steal (priv->widgets, GUINT_TO_POINTER (widget_id)); + + if (widget != NULL && !gtk_widget_in_destruction (widget)) + { + GtkWidget *parent = gtk_widget_get_ancestor (widget, GTK_TYPE_LIST_BOX_ROW); + + /* in case we added our own row ancestor, destroy it */ + if (parent != NULL && !gtk_widget_in_destruction (parent)) + gtk_widget_destroy (parent); + else + gtk_widget_destroy (widget); + } + + return TRUE; + } + + return FALSE; +} + +static void +dzl_preferences_view_set_page (DzlPreferences *preferences, + const gchar *page_name, + GHashTable *map) +{ + DzlPreferencesView *self = (DzlPreferencesView *)preferences; + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + GtkWidget *page; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + g_assert (page_name != NULL); + + page = dzl_preferences_view_get_page (self, page_name); + + if (page == NULL) + { + g_warning ("No such page \"%s\"", page_name); + return; + } + + if (strchr (page_name, '.') != NULL) + { + dzl_preferences_page_set_map (DZL_PREFERENCES_PAGE (page), map); + gtk_stack_set_visible_child (priv->subpage_stack, page); + gtk_widget_show (GTK_WIDGET (priv->subpage_stack)); + } + else + { + gtk_stack_set_visible_child (priv->page_stack, page); + gtk_widget_hide (GTK_WIDGET (priv->subpage_stack)); + } +} + +static GtkWidget * +dzl_preferences_view_get_widget (DzlPreferences *preferences, + guint widget_id) +{ + DzlPreferencesView *self = (DzlPreferencesView *)preferences; + DzlPreferencesViewPrivate *priv = dzl_preferences_view_get_instance_private (self); + TrackedWidget *tracked; + + g_assert (DZL_IS_PREFERENCES_VIEW (self)); + + tracked = g_hash_table_lookup (priv->widgets, GINT_TO_POINTER (widget_id)); + + return tracked ? tracked->widget : NULL; +} + +static void +dzl_preferences_iface_init (DzlPreferencesInterface *iface) +{ + iface->add_page = dzl_preferences_view_add_page; + iface->add_group = dzl_preferences_view_add_group; + iface->add_list_group = dzl_preferences_view_add_list_group; + iface->add_radio = dzl_preferences_view_add_radio; + iface->add_font_button = dzl_preferences_view_add_font_button; + iface->add_switch = dzl_preferences_view_add_switch; + iface->add_spin_button = dzl_preferences_view_add_spin_button; + iface->add_file_chooser = dzl_preferences_view_add_file_chooser; + iface->add_custom = dzl_preferences_view_add_custom; + iface->set_page = dzl_preferences_view_set_page; + iface->remove_id = dzl_preferences_view_remove_id; + iface->get_widget = dzl_preferences_view_get_widget; +} + +GtkWidget * +dzl_preferences_view_new (void) +{ + return g_object_new (DZL_TYPE_PREFERENCES_VIEW, NULL); +} diff --git a/src/prefs/dzl-preferences-view.h b/src/prefs/dzl-preferences-view.h new file mode 100644 index 0000000..417a50e --- /dev/null +++ b/src/prefs/dzl-preferences-view.h @@ -0,0 +1,56 @@ +/* dzl-preferences-view.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_VIEW_H +#define DZL_PREFERENCES_VIEW_H + +#include + +#include "dzl-version-macros.h" + +#include "prefs/dzl-preferences.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PREFERENCES_VIEW (dzl_preferences_view_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlPreferencesView, dzl_preferences_view, DZL, PREFERENCES_VIEW, GtkBin) + +struct _DzlPreferencesViewClass +{ + GtkBinClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_3_28 +GtkWidget *dzl_preferences_view_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_preferences_view_reapply_filter (DzlPreferencesView *self); + +G_END_DECLS + +#endif /* DZL_PREFERENCES_VIEW_H */ diff --git a/src/prefs/dzl-preferences-view.ui b/src/prefs/dzl-preferences-view.ui new file mode 100644 index 0000000..25bb738 --- /dev/null +++ b/src/prefs/dzl-preferences-view.ui @@ -0,0 +1,75 @@ + + + + + diff --git a/src/prefs/dzl-preferences.c b/src/prefs/dzl-preferences.c new file mode 100644 index 0000000..a783e67 --- /dev/null +++ b/src/prefs/dzl-preferences.c @@ -0,0 +1,264 @@ +/* dzl-preferences.c + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-preferences" + +#include "config.h" + +#include + +#include "prefs/dzl-preferences.h" + +G_DEFINE_INTERFACE (DzlPreferences, dzl_preferences, G_TYPE_OBJECT) + +static void +dzl_preferences_default_init (DzlPreferencesInterface *iface) +{ +} + +void +dzl_preferences_add_page (DzlPreferences *self, + const gchar *page_name, + const gchar *title, + gint priority) +{ + g_return_if_fail (DZL_IS_PREFERENCES (self)); + g_return_if_fail (page_name != NULL); + g_return_if_fail ((title != NULL) || (strchr (page_name, '.') != NULL)); + + DZL_PREFERENCES_GET_IFACE (self)->add_page (self, page_name, title, priority); +} + +void +dzl_preferences_add_group (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *title, + gint priority) +{ + g_return_if_fail (DZL_IS_PREFERENCES (self)); + g_return_if_fail (page_name != NULL); + g_return_if_fail (group_name != NULL); + + DZL_PREFERENCES_GET_IFACE (self)->add_group (self, page_name, group_name, title, priority); +} + +/** + * dzl_preferences_add_switch: + * @path: (nullable): An optional path + * @variant_string: (nullable): An optional gvariant string + * @title: (nullable): An optional title + * @subtitle: (nullable): An optional subtitle + * @keywords: (nullable): Optional keywords for search + * + */ +guint +dzl_preferences_add_switch (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *variant_string, + const gchar *title, + const gchar *subtitle, + const gchar *keywords, + gint priority) +{ + g_return_val_if_fail (DZL_IS_PREFERENCES (self), 0); + g_return_val_if_fail (page_name != NULL, 0); + g_return_val_if_fail (group_name != NULL, 0); + g_return_val_if_fail (schema_id != NULL, 0); + g_return_val_if_fail (key != NULL, 0); + g_return_val_if_fail (title != NULL, 0); + + return DZL_PREFERENCES_GET_IFACE (self)->add_switch (self, page_name, group_name, schema_id, key, path, variant_string, title, subtitle, keywords, priority); +} + +guint +dzl_preferences_add_spin_button (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *title, + const gchar *subtitle, + const gchar *keywords, + gint priority) +{ + g_return_val_if_fail (DZL_IS_PREFERENCES (self), 0); + g_return_val_if_fail (page_name != NULL, 0); + g_return_val_if_fail (group_name != NULL, 0); + g_return_val_if_fail (schema_id != NULL, 0); + g_return_val_if_fail (key != NULL, 0); + g_return_val_if_fail (title != NULL, 0); + + return DZL_PREFERENCES_GET_IFACE (self)->add_spin_button (self, page_name, group_name, schema_id, key, path, title, subtitle, keywords, priority); +} + +/** + * dzl_preferences_add_custom: + * @keywords: (nullable): Optional keywords for search + * + */ +guint +dzl_preferences_add_custom (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + GtkWidget *widget, + const gchar *keywords, + gint priority) +{ + g_return_val_if_fail (DZL_IS_PREFERENCES (self), 0); + g_return_val_if_fail (page_name != NULL, 0); + g_return_val_if_fail (group_name != NULL, 0); + g_return_val_if_fail (GTK_IS_WIDGET (widget), 0); + + return DZL_PREFERENCES_GET_IFACE (self)->add_custom (self, page_name, group_name, widget, keywords, priority); +} + +void +dzl_preferences_add_list_group (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *title, + GtkSelectionMode mode, + gint priority) +{ + g_return_if_fail (DZL_IS_PREFERENCES (self)); + g_return_if_fail (page_name != NULL); + g_return_if_fail (group_name != NULL); + + return DZL_PREFERENCES_GET_IFACE (self)->add_list_group (self, page_name, group_name, title, mode, priority); +} + +/** + * dzl_preferences_add_radio: + * @path: (nullable): An optional path + * @variant_string: (nullable): An optional gvariant string + * @title: (nullable): An optional title + * @subtitle: (nullable): An optional subtitle + * @keywords: (nullable): Optional keywords for search + * + */ +guint +dzl_preferences_add_radio (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *variant_string, + const gchar *title, + const gchar *subtitle, + const gchar *keywords, + gint priority) +{ + g_return_val_if_fail (DZL_IS_PREFERENCES (self), 0); + g_return_val_if_fail (page_name != NULL, 0); + g_return_val_if_fail (group_name != NULL, 0); + g_return_val_if_fail (schema_id != NULL, 0); + g_return_val_if_fail (key != NULL, 0); + g_return_val_if_fail (title != NULL, 0); + + return DZL_PREFERENCES_GET_IFACE (self)->add_radio (self, page_name, group_name, schema_id, key, path, variant_string, title, subtitle, keywords, priority); +} + +guint +dzl_preferences_add_font_button (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *title, + const gchar *keywords, + gint priority) +{ + g_return_val_if_fail (DZL_IS_PREFERENCES (self), 0); + g_return_val_if_fail (page_name != NULL, 0); + g_return_val_if_fail (group_name != NULL, 0); + g_return_val_if_fail (schema_id != NULL, 0); + g_return_val_if_fail (key != NULL, 0); + g_return_val_if_fail (title != NULL, 0); + + return DZL_PREFERENCES_GET_IFACE (self)->add_font_button (self, page_name, group_name, schema_id, key, title, keywords, priority); +} + +guint +dzl_preferences_add_file_chooser (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *title, + const gchar *subtitle, + GtkFileChooserAction action, + const gchar *keywords, + gint priority) +{ + g_return_val_if_fail (DZL_IS_PREFERENCES (self), 0); + g_return_val_if_fail (page_name != NULL, 0); + g_return_val_if_fail (group_name != NULL, 0); + g_return_val_if_fail (schema_id != NULL, 0); + g_return_val_if_fail (key != NULL, 0); + g_return_val_if_fail (title != NULL, 0); + + return DZL_PREFERENCES_GET_IFACE (self)->add_file_chooser (self, page_name, group_name, schema_id, key, path, title, subtitle, action, keywords, priority); +} + +/** + * ide_preference_remove_id: + * @widget_id: An preferences widget id + * + */ +gboolean +dzl_preferences_remove_id (DzlPreferences *self, + guint widget_id) +{ + g_return_val_if_fail (DZL_IS_PREFERENCES (self), FALSE); + g_return_val_if_fail (widget_id, FALSE); + + return DZL_PREFERENCES_GET_IFACE (self)->remove_id (self, widget_id); +} + +void +dzl_preferences_set_page (DzlPreferences *self, + const gchar *page_name, + GHashTable *map) +{ + g_return_if_fail (DZL_IS_PREFERENCES (self)); + g_return_if_fail (page_name != NULL); + + DZL_PREFERENCES_GET_IFACE (self)->set_page (self, page_name, map); +} + +/** + * dzl_preferences_get_widget: + * + * Returns: (transfer none) (nullable): A #GtkWidget or %NULL. + */ +GtkWidget * +dzl_preferences_get_widget (DzlPreferences *self, + guint widget_id) +{ + g_return_val_if_fail (DZL_IS_PREFERENCES (self), NULL); + + return DZL_PREFERENCES_GET_IFACE (self)->get_widget (self, widget_id); +} diff --git a/src/prefs/dzl-preferences.h b/src/prefs/dzl-preferences.h new file mode 100644 index 0000000..de08c8a --- /dev/null +++ b/src/prefs/dzl-preferences.h @@ -0,0 +1,212 @@ +/* dzl-preferences.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PREFERENCES_H +#define DZL_PREFERENCES_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PREFERENCES (dzl_preferences_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_INTERFACE (DzlPreferences, dzl_preferences, DZL, PREFERENCES, GObject) + +struct _DzlPreferencesInterface +{ + GTypeInterface parent_interface; + + void (*set_page) (DzlPreferences *self, + const gchar *page_name, + GHashTable *map); + void (*add_page) (DzlPreferences *self, + const gchar *page_name, + const gchar *title, + gint priority); + void (*add_group) (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *title, + gint priority); + void (*add_list_group) (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *title, + GtkSelectionMode mode, + gint priority); + guint (*add_radio) (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *variant_string, + const gchar *title, + const gchar *subtitle, + const gchar *keywords, + gint priority); + guint (*add_font_button) (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *title, + const gchar *keywords, + gint priority); + guint (*add_switch) (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *variant_string, + const gchar *title, + const gchar *subtitle, + const gchar *keywords, + gint priority); + guint (*add_spin_button) (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *title, + const gchar *subtitle, + const gchar *keywords, + gint priority); + guint (*add_file_chooser) (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *title, + const gchar *subtitle, + GtkFileChooserAction action, + const gchar *keywords, + gint priority); + guint (*add_custom) (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + GtkWidget *widget, + const gchar *keywords, + gint priority); + gboolean (*remove_id) (DzlPreferences *self, + guint widget_id); + GtkWidget *(*get_widget) (DzlPreferences *self, + guint widget_id); +}; + +DZL_AVAILABLE_IN_ALL +void dzl_preferences_add_page (DzlPreferences *self, + const gchar *page_name, + const gchar *title, + gint priority); +DZL_AVAILABLE_IN_ALL +void dzl_preferences_add_group (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *title, + gint priority); +DZL_AVAILABLE_IN_ALL +void dzl_preferences_add_list_group (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *title, + GtkSelectionMode mode, + gint priority); +DZL_AVAILABLE_IN_ALL +guint dzl_preferences_add_radio (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *variant_string, + const gchar *title, + const gchar *subtitle, + const gchar *keywords, + gint priority); +DZL_AVAILABLE_IN_ALL +guint dzl_preferences_add_switch (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *variant_string, + const gchar *title, + const gchar *subtitle, + const gchar *keywords, + gint priority); +DZL_AVAILABLE_IN_ALL +guint dzl_preferences_add_spin_button (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *title, + const gchar *subtitle, + const gchar *keywords, + gint priority); +DZL_AVAILABLE_IN_ALL +guint dzl_preferences_add_custom (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + GtkWidget *widget, + const gchar *keywords, + gint priority); +DZL_AVAILABLE_IN_ALL +guint dzl_preferences_add_font_button (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *title, + const gchar *keywords, + gint priority); +DZL_AVAILABLE_IN_ALL +guint dzl_preferences_add_file_chooser (DzlPreferences *self, + const gchar *page_name, + const gchar *group_name, + const gchar *schema_id, + const gchar *key, + const gchar *path, + const gchar *title, + const gchar *subtitle, + GtkFileChooserAction action, + const gchar *keywords, + gint priority); +DZL_AVAILABLE_IN_ALL +gboolean dzl_preferences_remove_id (DzlPreferences *self, + guint widget_id); +DZL_AVAILABLE_IN_ALL +void dzl_preferences_set_page (DzlPreferences *self, + const gchar *page_name, + GHashTable *map); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_preferences_get_widget (DzlPreferences *self, + guint widget_id); + +G_END_DECLS + +#endif /* DZL_PREFERENCES_H */ diff --git a/src/prefs/meson.build b/src/prefs/meson.build new file mode 100644 index 0000000..94ecd76 --- /dev/null +++ b/src/prefs/meson.build @@ -0,0 +1,32 @@ +prefs_headers = [ + 'dzl-preferences-bin.h', + 'dzl-preferences-entry.h', + 'dzl-preferences-file-chooser-button.h', + 'dzl-preferences-flow-box.h', + 'dzl-preferences-font-button.h', + 'dzl-preferences-group.h', + 'dzl-preferences-page.h', + 'dzl-preferences-spin-button.h', + 'dzl-preferences-switch.h', + 'dzl-preferences-view.h', + 'dzl-preferences.h', +] + +prefs_sources = [ + 'dzl-preferences-bin.c', + 'dzl-preferences-entry.c', + 'dzl-preferences-file-chooser-button.c', + 'dzl-preferences-flow-box.c', + 'dzl-preferences-font-button.c', + 'dzl-preferences-group.c', + 'dzl-preferences-page.c', + 'dzl-preferences-spin-button.c', + 'dzl-preferences-switch.c', + 'dzl-preferences-view.c', + 'dzl-preferences.c', +] + +libdazzle_public_headers += files(prefs_headers) +libdazzle_public_sources += files(prefs_sources) + +install_headers(prefs_headers, subdir: join_paths(libdazzle_header_subdir, 'prefs')) diff --git a/src/search/dzl-fuzzy-index-builder.c b/src/search/dzl-fuzzy-index-builder.c new file mode 100644 index 0000000..b381503 --- /dev/null +++ b/src/search/dzl-fuzzy-index-builder.c @@ -0,0 +1,693 @@ +/* dzl-fuzzy-index-builder.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-fuzzy-index-builder" +#define MAX_KEY_ENTRIES (0x00FFFFFF) + +#include "config.h" + +#include +#include + +#include "search/dzl-fuzzy-index-builder.h" +#include "util/dzl-variant.h" + +struct _DzlFuzzyIndexBuilder +{ + GObject object; + + guint case_sensitive : 1; + + /* + * This hash table contains a mapping of GVariants so that we + * deduplicate insertions of the same document. This helps when + * we have indexes that contain multiple strings to the same + * piece of data. + */ + GHashTable *documents_hash; + + /* + * This array contains a pointer to the individual GVariants + * while building the index. When writing the index to disk, + * we create a fixed array from this array of varians. + */ + GPtrArray *documents; + + /* + * Since we will need to keep a copy of a lot of strings, we + * use a GString chunk to reduce the presure on the allocator. + * It can certainly leave some gaps that are unused in the + * sequence of pages, but it is generally better than using + * a GByteArray or some other pow^2 growing array. + */ + GStringChunk *strings; + + /* + * This maps a pointer to a string that is found in the strings + * string chunk to a key id (stored as a pointer). The input + * string must exist within strings as we use a direct hash from + * the input pointer to map to the string to save on the cost + * of key equality checks. + */ + GHashTable *key_ids; + + /* + * An array of keys where the index of the key is the "key_id" used + * in other structures. The pointer points to a key within the + * strings GStringChunk. + */ + GPtrArray *keys; + + /* + * This array maps our document id to a key id. When building the + * search index we use this to disambiguate between multiple + * documents pointing to the same document. + */ + GArray *kv_pairs; + + /* + * Metadata for the search index, which is stored as the "metadata" + * key in the final search index. You can use fuzzy_index_get_metadata() + * to retrieve values stored here. + * + * This might be useful to store things like the mtime of the data + * you are indexes so that you know if you need to reindex. You might + * also store the version of your indexer here so that when you update + * your indexer code, you can force a rebuild of the index. + */ + GHashTable *metadata; +}; + +typedef struct +{ + /* The position within the keys array of the key. */ + guint key_id; + + /* The position within the documents array of the document */ + guint document_id; +} KVPair; + +typedef struct +{ + /* + * The character position within the string in terms of unicode + * characters, not byte-position. + */ + guint position; + + /* The index into the kvpairs */ + guint lookaside_id; +} IndexItem; + +G_DEFINE_TYPE (DzlFuzzyIndexBuilder, dzl_fuzzy_index_builder, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_CASE_SENSITIVE, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static guint +mask_priority (guint key_id) +{ + return key_id & 0x00FFFFFF; +} + +static void +dzl_fuzzy_index_builder_finalize (GObject *object) +{ + DzlFuzzyIndexBuilder *self = (DzlFuzzyIndexBuilder *)object; + + g_clear_pointer (&self->documents_hash, g_hash_table_unref); + g_clear_pointer (&self->documents, g_ptr_array_unref); + g_clear_pointer (&self->strings, g_string_chunk_free); + g_clear_pointer (&self->kv_pairs, g_array_unref); + g_clear_pointer (&self->metadata, g_hash_table_unref); + g_clear_pointer (&self->key_ids, g_hash_table_unref); + g_clear_pointer (&self->keys, g_ptr_array_unref); + + G_OBJECT_CLASS (dzl_fuzzy_index_builder_parent_class)->finalize (object); +} + +static void +dzl_fuzzy_index_builder_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlFuzzyIndexBuilder *self = DZL_FUZZY_INDEX_BUILDER (object); + + switch (prop_id) + { + case PROP_CASE_SENSITIVE: + g_value_set_boolean (value, dzl_fuzzy_index_builder_get_case_sensitive (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_fuzzy_index_builder_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlFuzzyIndexBuilder *self = DZL_FUZZY_INDEX_BUILDER (object); + + switch (prop_id) + { + case PROP_CASE_SENSITIVE: + dzl_fuzzy_index_builder_set_case_sensitive (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_fuzzy_index_builder_class_init (DzlFuzzyIndexBuilderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_fuzzy_index_builder_finalize; + object_class->get_property = dzl_fuzzy_index_builder_get_property; + object_class->set_property = dzl_fuzzy_index_builder_set_property; + + properties [PROP_CASE_SENSITIVE] = + g_param_spec_boolean ("case-sensitive", + "Case Sensitive", + "Case Sensitive", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_fuzzy_index_builder_init (DzlFuzzyIndexBuilder *self) +{ + self->documents = g_ptr_array_new_with_free_func ((GDestroyNotify)g_variant_unref); + self->documents_hash = g_hash_table_new (dzl_g_variant_hash, g_variant_equal); + self->kv_pairs = g_array_new (FALSE, FALSE, sizeof (KVPair)); + self->strings = g_string_chunk_new (4096); + self->key_ids = g_hash_table_new (NULL, NULL); + self->keys = g_ptr_array_new (); +} + +DzlFuzzyIndexBuilder * +dzl_fuzzy_index_builder_new (void) +{ + return g_object_new (DZL_TYPE_FUZZY_INDEX_BUILDER, NULL); +} + +/** + * dzl_fuzzy_index_builder_insert: + * @self: A #DzlFuzzyIndexBuilder + * @key: The UTF-8 encoded key for the document + * @document: The document to store + * @priority: An optional priority for the keyword. + * + * Inserts @document into the index using @key as the lookup key. + * + * If a matching document (checked by hashing @document) has already + * been inserted, only a single instance of the document will be stored. + * + * If @document is floating, it will be consumed. + * + * @priority may be used to group results by priority. Priority must be + * less than 256. + * + * Returns: The document id registered for @document. + */ +guint64 +dzl_fuzzy_index_builder_insert (DzlFuzzyIndexBuilder *self, + const gchar *key, + GVariant *document, + guint priority) +{ + g_autoptr(GVariant) sunk_variant = NULL; + GVariant *real_document = NULL; + gpointer document_id = NULL; + gpointer key_id = NULL; + KVPair pair; + + g_return_val_if_fail (DZL_IS_FUZZY_INDEX_BUILDER (self), 0L); + g_return_val_if_fail (key != NULL, 0L); + g_return_val_if_fail (document != NULL, 0L); + g_return_val_if_fail (priority <= 0xFF, 0L); + + if (g_variant_is_floating (document)) + sunk_variant = g_variant_ref_sink (document); + + /* move the priority bits into the proper area */ + priority = (priority & 0xFF) << 24; + + if (self->keys->len > MAX_KEY_ENTRIES) + { + g_warning ("Index is full, cannot add more entries"); + return 0L; + } + + key = g_string_chunk_insert_const (self->strings, key); + + /* + * We try to deduplicate document entries here by hashing the document and + * looking for another matching it. This way our generated index can stay + * relatively small when it comes to documents. + */ + if (!g_hash_table_lookup_extended (self->documents_hash, + document, + (gpointer *)&real_document, + &document_id)) + { + document_id = GUINT_TO_POINTER (self->documents->len); + real_document = g_variant_ref (document); + g_ptr_array_add (self->documents, real_document); + g_hash_table_insert (self->documents_hash, real_document, document_id); + } + + /* + * If we already have the key then reuse its key index. If not, then add it. + */ + if (!g_hash_table_lookup_extended (self->key_ids, key, NULL, &key_id)) + { + key_id = GUINT_TO_POINTER (self->keys->len); + g_ptr_array_add (self->keys, (gchar *)key); + g_hash_table_insert (self->key_ids, (gpointer)key, key_id); + } + + /* + * A bit of slight-of-hand here. We share keys between all key<->document + * pairs, but steal the high bits for the key priority in the kvpair entry. + * This allows for both deduplication and different priorities based on + * certain document pairs. + */ + pair.key_id = GPOINTER_TO_UINT (key_id) | priority; + pair.document_id = GPOINTER_TO_UINT (document_id); + + g_array_append_val (self->kv_pairs, pair); + + return pair.document_id; +} + +static gint +pos_doc_pair_compare (gconstpointer a, + gconstpointer b) +{ + const IndexItem *paira = a; + const IndexItem *pairb = b; + gint ret; + + ret = paira->lookaside_id - pairb->lookaside_id; + + if (ret == 0) + ret = paira->position - pairb->position; + + return ret; +} + +static GVariant * +dzl_fuzzy_index_builder_build_keys (DzlFuzzyIndexBuilder *self) +{ + g_assert (DZL_IS_FUZZY_INDEX_BUILDER (self)); + + return g_variant_new_strv ((const gchar * const *)self->keys->pdata, + self->keys->len); +} + +static GVariant * +dzl_fuzzy_index_builder_build_lookaside (DzlFuzzyIndexBuilder *self) +{ + g_assert (DZL_IS_FUZZY_INDEX_BUILDER (self)); + + return g_variant_new_fixed_array ((const GVariantType *)"(uu)", + self->kv_pairs->data, + self->kv_pairs->len, + sizeof (KVPair)); +} + +static GVariant * +dzl_fuzzy_index_builder_build_index (DzlFuzzyIndexBuilder *self) +{ + g_autoptr(GHashTable) rows = NULL; + GVariantDict dict; + GHashTableIter iter; + gpointer keyptr; + GArray *row; + guint i; + + g_assert (DZL_IS_FUZZY_INDEX_BUILDER (self)); + + rows = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_array_unref); + + for (i = 0; i < self->kv_pairs->len; i++) + { + g_autofree gchar *lower = NULL; + const gchar *key; + const gchar *tmp; + KVPair *kvpair; + IndexItem item; + guint position = 0; + + kvpair = &g_array_index (self->kv_pairs, KVPair, i); + key = g_ptr_array_index (self->keys, mask_priority (kvpair->key_id)); + + /* the priority for the key is stashed in the high 8 bits of + * the kvpair.key_id. So we need to propagate that to the + * entry in the index for resolution later. + */ + item.lookaside_id = i | (kvpair->key_id & 0xFF000000); + + if (!self->case_sensitive) + key = lower = g_utf8_casefold (key, -1); + + for (tmp = key; *tmp; tmp = g_utf8_next_char (tmp)) + { + gunichar ch = g_utf8_get_char (tmp); + + row = g_hash_table_lookup (rows, GUINT_TO_POINTER (ch)); + + if G_UNLIKELY (row == NULL) + { + row = g_array_new (FALSE, FALSE, sizeof (IndexItem)); + g_hash_table_insert (rows, GUINT_TO_POINTER (ch), row); + } + + item.position = position++; + g_array_append_val (row, item); + } + } + + g_variant_dict_init (&dict, NULL); + + g_hash_table_iter_init (&iter, rows); + + while (g_hash_table_iter_next (&iter, &keyptr, (gpointer *)&row)) + { + gchar key[12]; + GVariant *variant; + gunichar ch = GPOINTER_TO_UINT (keyptr); + + key [g_unichar_to_utf8 (ch, key)] = 0; + + g_array_sort (row, pos_doc_pair_compare); + + variant = g_variant_new_fixed_array ((const GVariantType *)"(uu)", + row->data, + row->len, + sizeof (IndexItem)); + g_variant_dict_insert_value (&dict, key, variant); + } + + return g_variant_dict_end (&dict); +} + +static GVariant * +dzl_fuzzy_index_builder_build_metadata (DzlFuzzyIndexBuilder *self) +{ + GVariantDict dict; + GHashTableIter iter; + + g_assert (DZL_IS_FUZZY_INDEX_BUILDER (self)); + + g_variant_dict_init (&dict, NULL); + + if (self->metadata != NULL) + { + const gchar *key; + GVariant *value; + + g_hash_table_iter_init (&iter, self->metadata); + while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value)) + g_variant_dict_insert_value (&dict, key, value); + } + + g_variant_dict_insert (&dict, "case-sensitive", "b", self->case_sensitive); + + return g_variant_dict_end (&dict); +} + +static void +dzl_fuzzy_index_builder_write_worker (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + DzlFuzzyIndexBuilder *self = source_object; + g_autoptr(GVariant) variant = NULL; + g_autoptr(GVariant) documents = NULL; + g_autoptr(GError) error = NULL; + GVariantDict dict; + GFile *file = task_data; + + g_assert (G_IS_TASK (task)); + g_assert (DZL_IS_FUZZY_INDEX_BUILDER (self)); + g_assert (G_IS_FILE (file)); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + g_variant_dict_init (&dict, NULL); + + /* Set our version number for the document */ + g_variant_dict_insert (&dict, "version", "i", 1); + + /* Build our dicitionary of metadata */ + g_variant_dict_insert_value (&dict, + "metadata", + dzl_fuzzy_index_builder_build_metadata (self)); + + /* Keys is an array of string keys where the index is the "key_id" */ + g_variant_dict_insert_value (&dict, + "keys", + dzl_fuzzy_index_builder_build_keys (self)); + + /* The lookaside is a mapping of kvpair to the repsective keys and + * documents. This allows the tables to use the kvpair id as the value + * in the index so we can have both document deduplication as well as + * the ability to disambiguate the keys which point to the same + * document. The contents are "a{uu}". + */ + g_variant_dict_insert_value (&dict, + "lookaside", + dzl_fuzzy_index_builder_build_lookaside (self)); + + /* Build our dicitionary of character → [(pos,lookaside_id),..] tuples. + * The position is the utf8 character position within the string. + * The lookaside_id is the index within the lookaside buffer to locate + * the document_id or key_id. + */ + g_variant_dict_insert_value (&dict, + "tables", + dzl_fuzzy_index_builder_build_index (self)); + + /* + * The documents are stored as an array where the document identifier is + * their index position. We then use a lookaside buffer to map the insertion + * id to the document id. Otherwise, we can't disambiguate between two + * keys that insert the same document (as we deduplicate documents inserted + * into the index). + */ + documents = g_variant_new_array (NULL, + (GVariant * const *)self->documents->pdata, + self->documents->len); + g_variant_dict_insert_value (&dict, "documents", g_variant_ref_sink (documents)); + + /* Now write the variant to disk */ + variant = g_variant_ref_sink (g_variant_dict_end (&dict)); + if (!g_file_replace_contents (file, + g_variant_get_data (variant), + g_variant_get_size (variant), + NULL, + FALSE, + G_FILE_CREATE_NONE, + NULL, + cancellable, + &error)) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_boolean (task, TRUE); +} + +/** + * dzl_fuzzy_index_builder_write_async: + * @self: A #DzlFuzzyIndexBuilder + * @file: A #GFile to write the index to + * @io_priority: The priority for IO operations + * @cancellable: (nullable): An optional #GCancellable or %NULL + * @callback: A callback for completion or %NULL + * @user_data: User data for @callback + * + * Builds and writes the index to @file. The file format is a + * GVariant on disk and can be loaded and searched using + * #FuzzyIndex. + */ +void +dzl_fuzzy_index_builder_write_async (DzlFuzzyIndexBuilder *self, + GFile *file, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (DZL_IS_FUZZY_INDEX_BUILDER (self)); + g_return_if_fail (G_IS_FILE (file)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, dzl_fuzzy_index_builder_write_async); + g_task_set_priority (task, io_priority); + g_task_set_task_data (task, g_object_ref (file), g_object_unref); + g_task_run_in_thread (task, dzl_fuzzy_index_builder_write_worker); +} + +gboolean +dzl_fuzzy_index_builder_write_finish (DzlFuzzyIndexBuilder *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (DZL_IS_FUZZY_INDEX_BUILDER (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +dzl_fuzzy_index_builder_write (DzlFuzzyIndexBuilder *self, + GFile *file, + gint io_priority, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GTask) task = NULL; + + g_return_val_if_fail (DZL_IS_FUZZY_INDEX_BUILDER (self), FALSE); + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + task = g_task_new (self, cancellable, NULL, NULL); + g_task_set_source_tag (task, dzl_fuzzy_index_builder_write); + g_task_set_priority (task, io_priority); + g_task_set_task_data (task, g_object_ref (file), g_object_unref); + + dzl_fuzzy_index_builder_write_worker (task, self, file, cancellable); + + return g_task_propagate_boolean (task, error); +} + +/** + * dzl_fuzzy_index_builder_get_document: + * + * Returns the document that was inserted in a previous call to + * dzl_fuzzy_index_builder_insert(). + * + * Returns: (transfer none): A #GVariant + */ +const GVariant * +dzl_fuzzy_index_builder_get_document (DzlFuzzyIndexBuilder *self, + guint64 document_id) +{ + g_return_val_if_fail (DZL_IS_FUZZY_INDEX_BUILDER (self), NULL); + g_return_val_if_fail ((guint)document_id < self->documents->len, NULL); + + return g_ptr_array_index (self->documents, (guint)document_id); +} + +void +dzl_fuzzy_index_builder_set_metadata (DzlFuzzyIndexBuilder *self, + const gchar *key, + GVariant *value) +{ + g_return_if_fail (DZL_IS_FUZZY_INDEX_BUILDER (self)); + g_return_if_fail (key != NULL); + + if (self->metadata == NULL) + self->metadata = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify)g_variant_unref); + + if (value != NULL) + g_hash_table_insert (self->metadata, + g_strdup (key), + g_variant_ref_sink (value)); + else + g_hash_table_remove (self->metadata, key); +} + +void +dzl_fuzzy_index_builder_set_metadata_string (DzlFuzzyIndexBuilder *self, + const gchar *key, + const gchar *value) +{ + g_return_if_fail (DZL_IS_FUZZY_INDEX_BUILDER (self)); + g_return_if_fail (key != NULL); + g_return_if_fail (value != NULL); + + dzl_fuzzy_index_builder_set_metadata (self, key, g_variant_new_string (value)); +} + +void +dzl_fuzzy_index_builder_set_metadata_uint32 (DzlFuzzyIndexBuilder *self, + const gchar *key, + guint32 value) +{ + g_return_if_fail (DZL_IS_FUZZY_INDEX_BUILDER (self)); + g_return_if_fail (key != NULL); + + dzl_fuzzy_index_builder_set_metadata (self, key, g_variant_new_uint32 (value)); +} + +void +dzl_fuzzy_index_builder_set_metadata_uint64 (DzlFuzzyIndexBuilder *self, + const gchar *key, + guint64 value) +{ + g_return_if_fail (DZL_IS_FUZZY_INDEX_BUILDER (self)); + g_return_if_fail (key != NULL); + + dzl_fuzzy_index_builder_set_metadata (self, key, g_variant_new_uint64 (value)); +} + +gboolean +dzl_fuzzy_index_builder_get_case_sensitive (DzlFuzzyIndexBuilder *self) +{ + g_return_val_if_fail (DZL_IS_FUZZY_INDEX_BUILDER (self), FALSE); + + return self->case_sensitive; +} + +void +dzl_fuzzy_index_builder_set_case_sensitive (DzlFuzzyIndexBuilder *self, + gboolean case_sensitive) +{ + g_return_if_fail (DZL_IS_FUZZY_INDEX_BUILDER (self)); + + case_sensitive = !!case_sensitive; + + if (self->case_sensitive != case_sensitive) + { + self->case_sensitive = case_sensitive; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CASE_SENSITIVE]); + } +} diff --git a/src/search/dzl-fuzzy-index-builder.h b/src/search/dzl-fuzzy-index-builder.h new file mode 100644 index 0000000..f7b210b --- /dev/null +++ b/src/search/dzl-fuzzy-index-builder.h @@ -0,0 +1,84 @@ +/* dzl-fuzzy-index-builder.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_FUZZY_INDEX_BUILDER_H +#define DZL_FUZZY_INDEX_BUILDER_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_FUZZY_INDEX_BUILDER (dzl_fuzzy_index_builder_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlFuzzyIndexBuilder, dzl_fuzzy_index_builder, DZL, FUZZY_INDEX_BUILDER, GObject) + +DZL_AVAILABLE_IN_ALL +DzlFuzzyIndexBuilder *dzl_fuzzy_index_builder_new (void); +DZL_AVAILABLE_IN_ALL +gboolean dzl_fuzzy_index_builder_get_case_sensitive (DzlFuzzyIndexBuilder *self); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_index_builder_set_case_sensitive (DzlFuzzyIndexBuilder *self, + gboolean case_sensitive); +DZL_AVAILABLE_IN_ALL +guint64 dzl_fuzzy_index_builder_insert (DzlFuzzyIndexBuilder *self, + const gchar *key, + GVariant *document, + guint priority); +DZL_AVAILABLE_IN_ALL +gboolean dzl_fuzzy_index_builder_write (DzlFuzzyIndexBuilder *self, + GFile *file, + gint io_priority, + GCancellable *cancellable, + GError **error); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_index_builder_write_async (DzlFuzzyIndexBuilder *self, + GFile *file, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +DZL_AVAILABLE_IN_ALL +gboolean dzl_fuzzy_index_builder_write_finish (DzlFuzzyIndexBuilder *self, + GAsyncResult *result, + GError **error); +DZL_AVAILABLE_IN_ALL +const GVariant *dzl_fuzzy_index_builder_get_document (DzlFuzzyIndexBuilder *self, + guint64 document_id); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_index_builder_set_metadata (DzlFuzzyIndexBuilder *self, + const gchar *key, + GVariant *value); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_index_builder_set_metadata_string (DzlFuzzyIndexBuilder *self, + const gchar *key, + const gchar *value); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_index_builder_set_metadata_uint32 (DzlFuzzyIndexBuilder *self, + const gchar *key, + guint32 value); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_index_builder_set_metadata_uint64 (DzlFuzzyIndexBuilder *self, + const gchar *key, + guint64 value); + +G_END_DECLS + +#endif /* DZL_FUZZY_INDEX_BUILDER_H */ diff --git a/src/search/dzl-fuzzy-index-cursor.c b/src/search/dzl-fuzzy-index-cursor.c new file mode 100644 index 0000000..9063808 --- /dev/null +++ b/src/search/dzl-fuzzy-index-cursor.c @@ -0,0 +1,631 @@ +/* dzl-fuzzy-index-cursor.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-fuzzy-index-cursor" + +#include "config.h" + +#include + +#include "search/dzl-fuzzy-index-cursor.h" +#include "search/dzl-fuzzy-index-match.h" +#include "search/dzl-fuzzy-index-private.h" +#include "util/dzl-int-pair.h" + +struct _DzlFuzzyIndexCursor +{ + GObject object; + + DzlFuzzyIndex *index; + gchar *query; + GVariantDict *tables; + GArray *matches; + guint max_matches; + guint case_sensitive : 1; +}; + +typedef struct +{ + guint position; + guint lookaside_id; +} DzlFuzzyIndexItem; + +typedef struct +{ + const gchar *key; + guint document_id; + gfloat score; + guint priority; +} DzlFuzzyMatch; + +typedef struct +{ + DzlFuzzyIndex *index; + const DzlFuzzyIndexItem * const *tables; + const gsize *tables_n_elements; + gint *tables_state; + guint n_tables; + guint max_matches; + const gchar *needle; + GHashTable *matches; +} DzlFuzzyLookup; + +enum { + PROP_0, + PROP_CASE_SENSITIVE, + PROP_INDEX, + PROP_TABLES, + PROP_MAX_MATCHES, + PROP_QUERY, + N_PROPS +}; + +static void async_initable_iface_init (GAsyncInitableIface *iface); +static void list_model_iface_init (GListModelInterface *iface); + +static GParamSpec *properties [N_PROPS]; + +G_DEFINE_TYPE_WITH_CODE (DzlFuzzyIndexCursor, dzl_fuzzy_index_cursor, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init) + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)) + +static inline gfloat +pointer_to_float (gpointer ptr) +{ + union { + gpointer ptr; +#if GLIB_SIZEOF_VOID_P == 8 + gdouble fval; +#else + gfloat fval; +#endif + } convert; + convert.ptr = ptr; + return (gfloat)convert.fval; +} + +static inline gpointer +float_to_pointer (gfloat fval) +{ + union { + gpointer ptr; +#if GLIB_SIZEOF_VOID_P == 8 + gdouble fval; +#else + gfloat fval; +#endif + } convert; + convert.fval = fval; + return convert.ptr; +} + +/* Not guaranteed, so assert to be sure */ +G_STATIC_ASSERT (sizeof(gdouble) == 8); +G_STATIC_ASSERT (sizeof(gfloat) == 4); + +static void +dzl_fuzzy_index_cursor_finalize (GObject *object) +{ + DzlFuzzyIndexCursor *self = (DzlFuzzyIndexCursor *)object; + + g_clear_object (&self->index); + g_clear_pointer (&self->query, g_free); + g_clear_pointer (&self->matches, g_array_unref); + g_clear_pointer (&self->tables, g_variant_dict_unref); + + G_OBJECT_CLASS (dzl_fuzzy_index_cursor_parent_class)->finalize (object); +} + +static void +dzl_fuzzy_index_cursor_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlFuzzyIndexCursor *self = DZL_FUZZY_INDEX_CURSOR(object); + + switch (prop_id) + { + case PROP_CASE_SENSITIVE: + g_value_set_boolean (value, self->case_sensitive); + break; + + case PROP_INDEX: + g_value_set_object (value, self->index); + break; + + case PROP_MAX_MATCHES: + g_value_set_uint (value, self->max_matches); + break; + + case PROP_QUERY: + g_value_set_string (value, self->query); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_fuzzy_index_cursor_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlFuzzyIndexCursor *self = DZL_FUZZY_INDEX_CURSOR(object); + + switch (prop_id) + { + case PROP_CASE_SENSITIVE: + self->case_sensitive = g_value_get_boolean (value); + break; + + case PROP_INDEX: + self->index = g_value_dup_object (value); + break; + + case PROP_TABLES: + self->tables = g_value_dup_boxed (value); + break; + + case PROP_MAX_MATCHES: + self->max_matches = g_value_get_uint (value); + break; + + case PROP_QUERY: + self->query = g_value_dup_string (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_fuzzy_index_cursor_class_init (DzlFuzzyIndexCursorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_fuzzy_index_cursor_finalize; + object_class->get_property = dzl_fuzzy_index_cursor_get_property; + object_class->set_property = dzl_fuzzy_index_cursor_set_property; + + properties [PROP_CASE_SENSITIVE] = + g_param_spec_boolean ("case-sensitive", + "Case Sensitive", + "Case Sensitive", + FALSE, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_INDEX] = + g_param_spec_object ("index", + "Index", + "The index this cursor is iterating", + DZL_TYPE_FUZZY_INDEX, + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TABLES] = + g_param_spec_boxed ("tables", + "Tables", + "The dictionary of character indexes", + G_TYPE_VARIANT_DICT, + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_QUERY] = + g_param_spec_string ("query", + "Query", + "The query for the index", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_MAX_MATCHES] = + g_param_spec_uint ("max-matches", + "Max Matches", + "The max number of matches to display", + 0, + G_MAXUINT, + 0, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_fuzzy_index_cursor_init (DzlFuzzyIndexCursor *self) +{ + self->matches = g_array_new (FALSE, FALSE, sizeof (DzlFuzzyMatch)); +} + +static gint +fuzzy_match_compare (gconstpointer a, + gconstpointer b) +{ + const DzlFuzzyMatch *ma = a; + const DzlFuzzyMatch *mb = b; + + if (ma->score < mb->score) + return 1; + else if (ma->score > mb->score) + return -1; + + return strcmp (ma->key, mb->key); +} + +static gboolean +fuzzy_do_match (const DzlFuzzyLookup *lookup, + const DzlFuzzyIndexItem *item, + guint table_index, + gint score) +{ + const DzlFuzzyIndexItem *table; + const DzlFuzzyIndexItem *iter; + gssize n_elements; + gint *state; + gint iter_score; + + g_assert (lookup != NULL); + g_assert (item != NULL); + g_assert (score >= 0); + + table = lookup->tables [table_index]; + n_elements = (gssize)lookup->tables_n_elements [table_index]; + state = &lookup->tables_state [table_index]; + + for (; state [0] < n_elements; state [0]++) + { + DzlIntPair *lookup_pair; + gboolean contains_document; + + iter = &table [state [0]]; + + if ((iter->lookaside_id < item->lookaside_id) || + ((iter->lookaside_id == item->lookaside_id) && + (iter->position <= item->position))) + continue; + else if (iter->lookaside_id > item->lookaside_id) + break; + + iter_score = score + (iter->position - item->position); + + if (table_index + 1 < lookup->n_tables) + { + if (fuzzy_do_match (lookup, iter, table_index + 1, iter_score)) + return TRUE; + continue; + } + + contains_document = g_hash_table_lookup_extended (lookup->matches, + GUINT_TO_POINTER (item->lookaside_id), + NULL, + (gpointer *)&lookup_pair); + + if (!contains_document || iter_score < dzl_int_pair_first (lookup_pair)) + g_hash_table_insert (lookup->matches, + GUINT_TO_POINTER (item->lookaside_id), + dzl_int_pair_new (iter_score, iter->position)); + + return TRUE; + } + + return FALSE; +} + +static void +dzl_fuzzy_index_cursor_worker (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + DzlFuzzyIndexCursor *self = source_object; + g_autoptr(GHashTable) matches = NULL; + g_autoptr(GHashTable) by_document = NULL; + g_autoptr(GPtrArray) tables = NULL; + g_autoptr(GArray) tables_n_elements = NULL; + g_autofree gint *tables_state = NULL; + g_autofree gchar *freeme = NULL; + const gchar *query; + DzlFuzzyLookup lookup = { 0 }; + GHashTableIter iter; + const gchar *str; + gpointer key, value; + guint i; + + g_assert (DZL_IS_FUZZY_INDEX_CURSOR (self)); + g_assert (G_IS_TASK (task)); + + if (g_task_return_error_if_cancelled (task)) + return; + + /* No matches with empty query */ + if (self->query == NULL || *self->query == '\0') + goto cleanup; + + /* If we are not case-sensitive, we need to downcase the query string */ + query = self->query; + if (!self->case_sensitive) + query = freeme = g_utf8_casefold (query, -1); + + tables = g_ptr_array_new (); + tables_n_elements = g_array_new (FALSE, FALSE, sizeof (gsize)); + matches = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)dzl_int_pair_free); + + for (str = query; *str; str = g_utf8_next_char (str)) + { + gunichar ch = g_utf8_get_char (str); + g_autoptr(GVariant) table = NULL; + gconstpointer fixed; + gsize n_elements; + gchar char_key[8]; + + if (g_unichar_isspace (ch)) + continue; + + char_key [g_unichar_to_utf8 (ch, char_key)] = '\0'; + table = g_variant_dict_lookup_value (self->tables, + char_key, + (const GVariantType *)"a(uu)"); + + /* No possible matches, missing table for character */ + if (table == NULL) + goto cleanup; + + fixed = g_variant_get_fixed_array (table, &n_elements, sizeof (DzlFuzzyIndexItem)); + g_array_append_val (tables_n_elements, n_elements); + g_ptr_array_add (tables, (gpointer)fixed); + } + + if (tables->len == 0) + goto cleanup; + + g_assert (tables->len > 0); + g_assert (tables->len == tables_n_elements->len); + + tables_state = g_new0 (gint, tables->len); + + lookup.index = self->index; + lookup.matches = matches; + lookup.tables = (const DzlFuzzyIndexItem * const *)tables->pdata; + lookup.tables_n_elements = (const gsize *)(gpointer)tables_n_elements->data; + lookup.tables_state = tables_state; + lookup.n_tables = tables->len; + lookup.needle = query; + lookup.max_matches = self->max_matches; + + if G_LIKELY (lookup.n_tables > 1) + { + for (i = 0; i < lookup.tables_n_elements[0]; i++) + { + const DzlFuzzyIndexItem *item = &lookup.tables[0][i]; + + fuzzy_do_match (&lookup, item, 1, MIN (16, item->position * 2)); + } + } + else + { + guint last_id = G_MAXUINT; + + for (i = 0; i < lookup.tables_n_elements[0]; i++) + { + const DzlFuzzyIndexItem *item = &lookup.tables[0][i]; + DzlFuzzyMatch match; + + if (item->lookaside_id != last_id) + { + last_id = item->lookaside_id; + + if G_UNLIKELY (!_dzl_fuzzy_index_resolve (self->index, + item->lookaside_id, + &match.document_id, + &match.key, + &match.priority, + item->position, + item->position, + &match.score)) + continue; + + g_array_append_val (self->matches, match); + } + } + + goto cleanup; + } + + if (g_task_return_error_if_cancelled (task)) + return; + + by_document = g_hash_table_new (NULL, NULL); + + g_hash_table_iter_init (&iter, matches); + + while (g_hash_table_iter_next (&iter, &key, &value)) + { + DzlIntPair *pair = value; + guint score = dzl_int_pair_first (pair); + guint last_offset = dzl_int_pair_second (pair); + gpointer other_score; + DzlFuzzyMatch match; + guint lookaside_id = GPOINTER_TO_UINT (key); + + if G_UNLIKELY (!_dzl_fuzzy_index_resolve (self->index, + lookaside_id, + &match.document_id, + &match.key, + &match.priority, + score, + last_offset, + &match.score)) + continue; + + if (g_hash_table_lookup_extended (by_document, + GUINT_TO_POINTER (match.document_id), + NULL, + &other_score) && + match.score <= pointer_to_float (other_score)) + continue; + + g_hash_table_insert (by_document, + GUINT_TO_POINTER (match.document_id), + float_to_pointer (match.score)); + + g_array_append_val (self->matches, match); + } + + /* + * Because we have to do the deduplication of documents after + * searching all the potential matches, we could have duplicates + * in the array. So we need to filter them out. Not that big + * of a deal, since we can do remove_fast on the index and + * we have to sort them afterwards anyway. Potentially, we could + * do both at once if we felt like doing our own sort. + */ + for (i = 0; i < self->matches->len; i++) + { + DzlFuzzyMatch *match; + gpointer other_score; + + next: + match = &g_array_index (self->matches, DzlFuzzyMatch, i); + other_score = g_hash_table_lookup (by_document, GUINT_TO_POINTER (match->document_id)); + + /* + * This item should have been discarded, but we didn't have enough + * information at the time we built the array. + */ + if (pointer_to_float (other_score) < match->score) + { + g_array_remove_index_fast (self->matches, i); + if (i < self->matches->len - 1) + goto next; + } + } + + if (g_task_return_error_if_cancelled (task)) + return; + +cleanup: + if (self->matches != NULL) + { + g_array_sort (self->matches, fuzzy_match_compare); + if (lookup.max_matches > 0 && lookup.max_matches < self->matches->len) + g_array_set_size (self->matches, lookup.max_matches); + } + + g_task_return_boolean (task, TRUE); +} + +static void +dzl_fuzzy_index_cursor_init_async (GAsyncInitable *initable, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + DzlFuzzyIndexCursor *self = (DzlFuzzyIndexCursor *)initable; + g_autoptr(GTask) task = NULL; + + g_assert (DZL_IS_FUZZY_INDEX_CURSOR (self)); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, dzl_fuzzy_index_cursor_init_async); + g_task_set_priority (task, io_priority); + g_task_set_check_cancellable (task, FALSE); + g_task_run_in_thread (task, dzl_fuzzy_index_cursor_worker); +} + +static gboolean +dzl_fuzzy_index_cursor_init_finish (GAsyncInitable *initiable, + GAsyncResult *result, + GError **error) +{ + g_assert (DZL_IS_FUZZY_INDEX_CURSOR (initiable)); + g_assert (G_IS_TASK (result)); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +async_initable_iface_init (GAsyncInitableIface *iface) +{ + iface->init_async = dzl_fuzzy_index_cursor_init_async; + iface->init_finish = dzl_fuzzy_index_cursor_init_finish; +} + +static GType +dzl_fuzzy_index_cursor_get_item_type (GListModel *model) +{ + return DZL_TYPE_FUZZY_INDEX_MATCH; +} + +static guint +dzl_fuzzy_index_cursor_get_n_items (GListModel *model) +{ + DzlFuzzyIndexCursor *self = (DzlFuzzyIndexCursor *)model; + + g_assert (DZL_IS_FUZZY_INDEX_CURSOR (self)); + + return self->matches->len; +} + +static gpointer +dzl_fuzzy_index_cursor_get_item (GListModel *model, + guint position) +{ + DzlFuzzyIndexCursor *self = (DzlFuzzyIndexCursor *)model; + g_autoptr(GVariant) document = NULL; + DzlFuzzyMatch *match; + + g_assert (DZL_IS_FUZZY_INDEX_CURSOR (self)); + g_assert (position < self->matches->len); + + match = &g_array_index (self->matches, DzlFuzzyMatch, position); + + document = _dzl_fuzzy_index_lookup_document (self->index, match->document_id); + + return g_object_new (DZL_TYPE_FUZZY_INDEX_MATCH, + "document", document, + "key", match->key, + "score", match->score, + "priority", match->priority, + NULL); +} + +static void +list_model_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = dzl_fuzzy_index_cursor_get_item_type; + iface->get_n_items = dzl_fuzzy_index_cursor_get_n_items; + iface->get_item = dzl_fuzzy_index_cursor_get_item; +} + +/** + * dzl_fuzzy_index_cursor_get_index: + * @self: A #DzlFuzzyIndexCursor + * + * Gets the index the cursor is iterating. + * + * Returns: (transfer none): A #DzlFuzzyIndex. + */ +DzlFuzzyIndex * +dzl_fuzzy_index_cursor_get_index (DzlFuzzyIndexCursor *self) +{ + g_return_val_if_fail (DZL_IS_FUZZY_INDEX_CURSOR (self), NULL); + + return self->index; +} diff --git a/src/search/dzl-fuzzy-index-cursor.h b/src/search/dzl-fuzzy-index-cursor.h new file mode 100644 index 0000000..2da562b --- /dev/null +++ b/src/search/dzl-fuzzy-index-cursor.h @@ -0,0 +1,40 @@ +/* dzl-fuzzy-index-cursor.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_FUZZY_INDEX_CURSOR_H +#define DZL_FUZZY_INDEX_CURSOR_H + +#include + +#include "dzl-version-macros.h" + +#include "dzl-fuzzy-index.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_FUZZY_INDEX_CURSOR (dzl_fuzzy_index_cursor_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlFuzzyIndexCursor, dzl_fuzzy_index_cursor, DZL, FUZZY_INDEX_CURSOR, GObject) + +DZL_AVAILABLE_IN_ALL +DzlFuzzyIndex *dzl_fuzzy_index_cursor_get_index (DzlFuzzyIndexCursor *self); + +G_END_DECLS + +#endif /* DZL_FUZZY_INDEX_CURSOR_H */ diff --git a/src/search/dzl-fuzzy-index-match.c b/src/search/dzl-fuzzy-index-match.c new file mode 100644 index 0000000..1c0bb46 --- /dev/null +++ b/src/search/dzl-fuzzy-index-match.c @@ -0,0 +1,205 @@ +/* dzl-fuzzy-index-match.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-fuzzy-index-match" + +#include "config.h" + +#include "dzl-fuzzy-index-match.h" + +struct _DzlFuzzyIndexMatch +{ + GObject object; + GVariant *document; + gchar *key; + gfloat score; + guint priority; +}; + +enum { + PROP_0, + PROP_DOCUMENT, + PROP_KEY, + PROP_SCORE, + PROP_PRIORITY, + N_PROPS +}; + +G_DEFINE_TYPE (DzlFuzzyIndexMatch, dzl_fuzzy_index_match, G_TYPE_OBJECT) + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_fuzzy_index_match_finalize (GObject *object) +{ + DzlFuzzyIndexMatch *self = (DzlFuzzyIndexMatch *)object; + + g_clear_pointer (&self->document, g_variant_unref); + g_clear_pointer (&self->key, g_free); + + G_OBJECT_CLASS (dzl_fuzzy_index_match_parent_class)->finalize (object); +} + +static void +dzl_fuzzy_index_match_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlFuzzyIndexMatch *self = DZL_FUZZY_INDEX_MATCH (object); + + switch (prop_id) + { + case PROP_DOCUMENT: + g_value_set_variant (value, self->document); + break; + + case PROP_SCORE: + g_value_set_float (value, self->score); + break; + + case PROP_KEY: + g_value_set_string (value, self->key); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, self->priority); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_fuzzy_index_match_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlFuzzyIndexMatch *self = DZL_FUZZY_INDEX_MATCH (object); + + switch (prop_id) + { + case PROP_DOCUMENT: + self->document = g_value_dup_variant (value); + break; + + case PROP_SCORE: + self->score = g_value_get_float (value); + break; + + case PROP_KEY: + self->key = g_value_dup_string (value); + break; + + case PROP_PRIORITY: + self->priority = g_value_get_uint (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_fuzzy_index_match_class_init (DzlFuzzyIndexMatchClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_fuzzy_index_match_finalize; + object_class->get_property = dzl_fuzzy_index_match_get_property; + object_class->set_property = dzl_fuzzy_index_match_set_property; + + properties [PROP_DOCUMENT] = + g_param_spec_variant ("document", + "Document", + "Document", + G_VARIANT_TYPE_ANY, + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_KEY] = + g_param_spec_string ("key", + "Key", + "The string key that was inserted for the document", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_PRIORITY] = + g_param_spec_uint ("priority", + "Priority", + "The priority used when creating the index", + 0, + 255, + 0, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SCORE] = + g_param_spec_float ("score", + "Score", + "Score", + -G_MINFLOAT, + G_MAXFLOAT, + 0.0f, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_fuzzy_index_match_init (DzlFuzzyIndexMatch *match) +{ +} + +/** + * dzl_fuzzy_index_match_get_document: + * + * Returns: (transfer none): A #GVariant. + */ +GVariant * +dzl_fuzzy_index_match_get_document (DzlFuzzyIndexMatch *self) +{ + g_return_val_if_fail (DZL_IS_FUZZY_INDEX_MATCH (self), NULL); + + return self->document; +} + +gfloat +dzl_fuzzy_index_match_get_score (DzlFuzzyIndexMatch *self) +{ + g_return_val_if_fail (DZL_IS_FUZZY_INDEX_MATCH (self), 0.0f); + + return self->score; +} + +const gchar * +dzl_fuzzy_index_match_get_key (DzlFuzzyIndexMatch *self) +{ + g_return_val_if_fail (DZL_IS_FUZZY_INDEX_MATCH (self), NULL); + + return self->key; +} + +guint +dzl_fuzzy_index_match_get_priority (DzlFuzzyIndexMatch *self) +{ + g_return_val_if_fail (DZL_IS_FUZZY_INDEX_MATCH (self), 0); + + return self->priority; +} diff --git a/src/search/dzl-fuzzy-index-match.h b/src/search/dzl-fuzzy-index-match.h new file mode 100644 index 0000000..c6d09f5 --- /dev/null +++ b/src/search/dzl-fuzzy-index-match.h @@ -0,0 +1,44 @@ +/* dzl-fuzzy-index-match.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_FUZZY_INDEX_MATCH_H +#define DZL_FUZZY_INDEX_MATCH_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_FUZZY_INDEX_MATCH (dzl_fuzzy_index_match_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlFuzzyIndexMatch, dzl_fuzzy_index_match, DZL, FUZZY_INDEX_MATCH, GObject) + +DZL_AVAILABLE_IN_ALL +const gchar *dzl_fuzzy_index_match_get_key (DzlFuzzyIndexMatch *self); +DZL_AVAILABLE_IN_ALL +GVariant *dzl_fuzzy_index_match_get_document (DzlFuzzyIndexMatch *self); +DZL_AVAILABLE_IN_ALL +gfloat dzl_fuzzy_index_match_get_score (DzlFuzzyIndexMatch *self); +DZL_AVAILABLE_IN_ALL +guint dzl_fuzzy_index_match_get_priority (DzlFuzzyIndexMatch *self); + +G_END_DECLS + +#endif /* DZL_FUZZY_INDEX_MATCH_H */ diff --git a/src/search/dzl-fuzzy-index-private.h b/src/search/dzl-fuzzy-index-private.h new file mode 100644 index 0000000..ecbeea5 --- /dev/null +++ b/src/search/dzl-fuzzy-index-private.h @@ -0,0 +1,39 @@ +/* dzl-fuzzy-index-private.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_FUZZY_INDEX_PRIVATE_H +#define DZL_FUZZY_INDEX_PRIVATE_H + +#include "dzl-fuzzy-index.h" + +G_BEGIN_DECLS + +GVariant *_dzl_fuzzy_index_lookup_document (DzlFuzzyIndex *self, + guint document_id); +gboolean _dzl_fuzzy_index_resolve (DzlFuzzyIndex *self, + guint lookaside_id, + guint *document_id, + const gchar **key, + guint *priority, + guint in_score, + guint last_offset, + gfloat *out_score); + +G_END_DECLS + +#endif /* DZL_FUZZY_INDEX_PRIVATE_H */ diff --git a/src/search/dzl-fuzzy-index.c b/src/search/dzl-fuzzy-index.c new file mode 100644 index 0000000..f789271 --- /dev/null +++ b/src/search/dzl-fuzzy-index.c @@ -0,0 +1,508 @@ +/* dzl-fuzzy-index.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-fuzzy-index" + +#include "config.h" + +#include + +#include "dzl-fuzzy-index.h" +#include "dzl-fuzzy-index-cursor.h" +#include "dzl-fuzzy-index-private.h" + +typedef struct +{ + guint key_id; + guint document_id; +} LookasideEntry; + +struct _DzlFuzzyIndex +{ + GObject object; + + guint loaded : 1; + guint case_sensitive : 1; + + GMappedFile *mapped_file; + + /* + * Toplevel variant for the whole document. This is loaded from the entire + * contents of @mapped_file. It contains a dictionary of "a{sv}" + * containing all of our index data tables. + */ + GVariant *variant; + + /* + * This is a variant containing the array of documents. The index of the + * document is the corresponding document_id used in other data structres. + * This maps to the "documents" field in @variant. + */ + GVariant *documents; + + /* + * The keys found within the index. The index of the key is the "key_id" + * used in other datastructures, such as the @lookaside array. + */ + GVariant *keys; + + /* + * The lookaside array is used to disambiguate between multiple keys + * pointing to the same document. Each element in the array is of type + * "(uu)" with the first field being the "key_id" and the second field + * being the "document_id". Each of these are indexes into the + * corresponding @documents and @keys arrays. + * + * This is a fixed array type and therefore can have the raw data + * accessed with g_variant_get_fixed_array() to save on lookup + * costs. + */ + GVariant *lookaside; + + /* + * Raw pointers for fast access to the lookaside buffer. + */ + const LookasideEntry *lookaside_raw; + gsize lookaside_len; + + /* + * This vardict is used to get the fixed array containing the + * (offset, lookaside_id) for each unicode character in the index. + * These are accessed by the cursors to layout the fulltext search + * index by each character in the input string. Doing so, is what + * gives us the O(mn) worst-case running time. + */ + GVariantDict *tables; + + /* + * The metadata located within the search index. This contains + * metadata set with dzl_fuzzy_index_builder_set_metadata() or one + * of its typed variants. + */ + GVariantDict *metadata; +}; + +G_DEFINE_TYPE (DzlFuzzyIndex, dzl_fuzzy_index, G_TYPE_OBJECT) + +static void +dzl_fuzzy_index_finalize (GObject *object) +{ + DzlFuzzyIndex *self = (DzlFuzzyIndex *)object; + + g_clear_pointer (&self->mapped_file, g_mapped_file_unref); + g_clear_pointer (&self->variant, g_variant_unref); + g_clear_pointer (&self->documents, g_variant_unref); + g_clear_pointer (&self->keys, g_variant_unref); + g_clear_pointer (&self->tables, g_variant_dict_unref); + g_clear_pointer (&self->lookaside, g_variant_unref); + g_clear_pointer (&self->metadata, g_variant_dict_unref); + + G_OBJECT_CLASS (dzl_fuzzy_index_parent_class)->finalize (object); +} + +static void +dzl_fuzzy_index_class_init (DzlFuzzyIndexClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_fuzzy_index_finalize; +} + +static void +dzl_fuzzy_index_init (DzlFuzzyIndex *self) +{ +} + +DzlFuzzyIndex * +dzl_fuzzy_index_new (void) +{ + return g_object_new (DZL_TYPE_FUZZY_INDEX, NULL); +} + +static void +dzl_fuzzy_index_load_file_worker (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autofree gchar *path = NULL; + g_autoptr(GMappedFile) mapped_file = NULL; + g_autoptr(GVariant) variant = NULL; + g_autoptr(GVariant) documents = NULL; + g_autoptr(GVariant) lookaside = NULL; + g_autoptr(GVariant) keys = NULL; + g_autoptr(GVariant) tables = NULL; + g_autoptr(GVariant) metadata = NULL; + g_autoptr(GError) error = NULL; + DzlFuzzyIndex *self = source_object; + GFile *file = task_data; + GVariantDict dict; + gint version = 0; + gboolean case_sensitive = FALSE; + + g_assert (DZL_IS_FUZZY_INDEX (self)); + g_assert (G_IS_FILE (file)); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + if (self->loaded) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVAL, + "Cannot load index multiple times"); + return; + } + + self->loaded = TRUE; + + if (!g_file_is_native (file) || NULL == (path = g_file_get_path (file))) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVALID_FILENAME, + "Index must be a local file"); + return; + } + + if (NULL == (mapped_file = g_mapped_file_new (path, FALSE, &error))) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + variant = g_variant_new_from_data (G_VARIANT_TYPE_VARDICT, + g_mapped_file_get_contents (mapped_file), + g_mapped_file_get_length (mapped_file), + FALSE, NULL, NULL); + + if (variant == NULL) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVAL, + "Failed to parse GVariant"); + return; + } + + g_variant_ref_sink (variant); + + g_variant_dict_init (&dict, variant); + + if (!g_variant_dict_lookup (&dict, "version", "i", &version) || version != 1) + { + g_variant_dict_clear (&dict); + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVAL, + "Version mismatch in gvariant. Got %d, expected 1", + version); + return; + } + + documents = g_variant_dict_lookup_value (&dict, "documents", G_VARIANT_TYPE_ARRAY); + keys = g_variant_dict_lookup_value (&dict, "keys", G_VARIANT_TYPE_STRING_ARRAY); + lookaside = g_variant_dict_lookup_value (&dict, "lookaside", G_VARIANT_TYPE_ARRAY); + tables = g_variant_dict_lookup_value (&dict, "tables", G_VARIANT_TYPE_VARDICT); + metadata = g_variant_dict_lookup_value (&dict, "metadata", G_VARIANT_TYPE_VARDICT); + g_variant_dict_clear (&dict); + + if (keys == NULL || documents == NULL || tables == NULL || metadata == NULL) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVAL, + "Invalid gvariant index"); + return; + } + + self->mapped_file = g_steal_pointer (&mapped_file); + self->variant = g_steal_pointer (&variant); + self->documents = g_steal_pointer (&documents); + self->lookaside = g_steal_pointer (&lookaside); + self->keys = g_steal_pointer (&keys); + self->tables = g_variant_dict_new (tables); + self->metadata = g_variant_dict_new (metadata); + + self->lookaside_raw = g_variant_get_fixed_array (self->lookaside, + &self->lookaside_len, + sizeof (LookasideEntry)); + + if (g_variant_dict_lookup (self->metadata, "case-sensitive", "b", &case_sensitive)) + self->case_sensitive = !!case_sensitive; + + g_task_return_boolean (task, TRUE); +} + +void +dzl_fuzzy_index_load_file_async (DzlFuzzyIndex *self, + GFile *file, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (DZL_IS_FUZZY_INDEX (self)); + g_return_if_fail (G_IS_FILE (file)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, dzl_fuzzy_index_load_file); + g_task_set_task_data (task, g_object_ref (file), g_object_unref); + g_task_set_check_cancellable (task, FALSE); + g_task_run_in_thread (task, dzl_fuzzy_index_load_file_worker); +} + +gboolean +dzl_fuzzy_index_load_file_finish (DzlFuzzyIndex *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (DZL_IS_FUZZY_INDEX (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +dzl_fuzzy_index_load_file (DzlFuzzyIndex *self, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GTask) task = NULL; + + g_return_val_if_fail (DZL_IS_FUZZY_INDEX (self), FALSE); + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + task = g_task_new (self, cancellable, NULL, NULL); + g_task_set_source_tag (task, dzl_fuzzy_index_load_file); + g_task_set_task_data (task, g_object_ref (file), g_object_unref); + g_task_set_check_cancellable (task, FALSE); + + dzl_fuzzy_index_load_file_worker (task, self, file, cancellable); + + return g_task_propagate_boolean (task, error); +} + +static void +dzl_fuzzy_index_query_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + DzlFuzzyIndexCursor *cursor = (DzlFuzzyIndexCursor *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + + g_assert (DZL_IS_FUZZY_INDEX_CURSOR (cursor)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (G_IS_TASK (task)); + + if (!g_async_initable_init_finish (G_ASYNC_INITABLE (cursor), result, &error)) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_pointer (task, g_object_ref (cursor), g_object_unref); +} + +void +dzl_fuzzy_index_query_async (DzlFuzzyIndex *self, + const gchar *query, + guint max_matches, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(DzlFuzzyIndexCursor) cursor = NULL; + + g_return_if_fail (DZL_IS_FUZZY_INDEX (self)); + g_return_if_fail (query != NULL); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_priority (task, G_PRIORITY_LOW); + g_task_set_source_tag (task, dzl_fuzzy_index_query_async); + + cursor = g_object_new (DZL_TYPE_FUZZY_INDEX_CURSOR, + "case-sensitive", self->case_sensitive, + "index", self, + "query", query, + "max-matches", max_matches, + "tables", self->tables, + NULL); + + g_async_initable_init_async (G_ASYNC_INITABLE (cursor), + G_PRIORITY_LOW, + cancellable, + dzl_fuzzy_index_query_cb, + g_steal_pointer (&task)); +} + +/** + * dzl_fuzzy_index_query_finish: + * + * Completes an asynchronous request to dzl_fuzzy_index_query_async(). + * + * Returns: (transfer full): A #GListModel of results. + */ +GListModel * +dzl_fuzzy_index_query_finish (DzlFuzzyIndex *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (DZL_IS_FUZZY_INDEX (self), NULL); + g_return_val_if_fail (G_IS_TASK (result), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +/** + * dzl_fuzzy_index_get_metadata: + * + * Looks up the metadata for @key. + * + * Returns: (transfer full) (nullable): A #GVariant or %NULL. + */ +GVariant * +dzl_fuzzy_index_get_metadata (DzlFuzzyIndex *self, + const gchar *key) +{ + g_return_val_if_fail (DZL_IS_FUZZY_INDEX (self), NULL); + g_return_val_if_fail (key != NULL, NULL); + + if (self->metadata != NULL) + return g_variant_dict_lookup_value (self->metadata, key, NULL); + + return NULL; +} + +guint64 +dzl_fuzzy_index_get_metadata_uint64 (DzlFuzzyIndex *self, + const gchar *key) +{ + g_autoptr(GVariant) ret = NULL; + + ret = dzl_fuzzy_index_get_metadata (self, key); + + if (ret != NULL) + return g_variant_get_uint64 (ret); + + return G_GUINT64_CONSTANT (0); +} + +guint32 +dzl_fuzzy_index_get_metadata_uint32 (DzlFuzzyIndex *self, + const gchar *key) +{ + g_autoptr(GVariant) ret = NULL; + + ret = dzl_fuzzy_index_get_metadata (self, key); + + if (ret != NULL) + return g_variant_get_uint32 (ret); + + return G_GUINT64_CONSTANT (0); +} + +const gchar * +dzl_fuzzy_index_get_metadata_string (DzlFuzzyIndex *self, + const gchar *key) +{ + g_autoptr(GVariant) ret = NULL; + + ret = dzl_fuzzy_index_get_metadata (self, key); + + /* + * This looks unsafe, but is safe because strings are \0 terminated + * inside the variants and the result is a pointer to internal data. + * Since that data exists on our mmap() region, we are all good. + */ + if (ret != NULL) + return g_variant_get_string (ret, NULL); + + return NULL; +} + +/** + * _dzl_fuzzy_index_lookup_document: + * @self: A #DzlFuzzyIndex + * @document_id: The identifier of the document + * + * This looks up the document found matching @document_id. + * + * This should be the document_id resolved through the lookaside + * using _dzl_fuzzy_index_resolve(). + * + * Returns: (transfer full) (nullable): A #GVariant if successful; + * otherwise %NULL. + */ +GVariant * +_dzl_fuzzy_index_lookup_document (DzlFuzzyIndex *self, + guint document_id) +{ + g_assert (DZL_IS_FUZZY_INDEX (self)); + + return g_variant_get_child_value (self->documents, document_id); +} + +gboolean +_dzl_fuzzy_index_resolve (DzlFuzzyIndex *self, + guint lookaside_id, + guint *document_id, + const gchar **key, + guint *priority, + guint in_score, + guint last_offset, + gfloat *out_score) +{ + const LookasideEntry *entry; + const gchar *local_key = NULL; + guint key_id; + + g_assert (DZL_IS_FUZZY_INDEX (self)); + g_assert (document_id != NULL); + g_assert (out_score != NULL); + g_assert (priority != NULL); + + /* Mask off the key priority */ + lookaside_id &= 0x00FFFFFF; + + if G_UNLIKELY (lookaside_id >= self->lookaside_len) + return FALSE; + + entry = &self->lookaside_raw [lookaside_id]; + + /* The key_id has a mask with the priority as well */ + key_id = entry->key_id & 0x00FFFFFF; + if G_UNLIKELY (key_id >= g_variant_n_children (self->keys)) + return FALSE; + + g_variant_get_child (self->keys, key_id, "&s", &local_key); + + if (key != NULL) + *key = local_key; + + if (document_id != NULL) + *document_id = entry->document_id; + + *priority = (entry->key_id & 0xFF000000) >> 24; + *out_score = ((1.0 / 256.0) / (1 + last_offset + in_score)) + ((255.0 - *priority) / 256.0); + + return TRUE; +} diff --git a/src/search/dzl-fuzzy-index.h b/src/search/dzl-fuzzy-index.h new file mode 100644 index 0000000..8cc20c2 --- /dev/null +++ b/src/search/dzl-fuzzy-index.h @@ -0,0 +1,76 @@ +/* dzl-fuzzy-index.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_FUZZY_INDEX_H +#define DZL_FUZZY_INDEX_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_FUZZY_INDEX (dzl_fuzzy_index_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlFuzzyIndex, dzl_fuzzy_index, DZL, FUZZY_INDEX, GObject) + +DZL_AVAILABLE_IN_ALL +DzlFuzzyIndex *dzl_fuzzy_index_new (void); +DZL_AVAILABLE_IN_ALL +gboolean dzl_fuzzy_index_load_file (DzlFuzzyIndex *self, + GFile *file, + GCancellable *cancellable, + GError **error); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_index_load_file_async (DzlFuzzyIndex *self, + GFile *file, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +DZL_AVAILABLE_IN_ALL +gboolean dzl_fuzzy_index_load_file_finish (DzlFuzzyIndex *self, + GAsyncResult *result, + GError **error); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_index_query_async (DzlFuzzyIndex *self, + const gchar *query, + guint max_matches, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +DZL_AVAILABLE_IN_ALL +GListModel *dzl_fuzzy_index_query_finish (DzlFuzzyIndex *self, + GAsyncResult *result, + GError **error); +DZL_AVAILABLE_IN_ALL +GVariant *dzl_fuzzy_index_get_metadata (DzlFuzzyIndex *self, + const gchar *key); +DZL_AVAILABLE_IN_ALL +guint32 dzl_fuzzy_index_get_metadata_uint32 (DzlFuzzyIndex *self, + const gchar *key); +DZL_AVAILABLE_IN_ALL +guint64 dzl_fuzzy_index_get_metadata_uint64 (DzlFuzzyIndex *self, + const gchar *key); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_fuzzy_index_get_metadata_string (DzlFuzzyIndex *self, + const gchar *key); + +G_END_DECLS + +#endif /* DZL_FUZZY_INDEX_H */ diff --git a/src/search/dzl-fuzzy-mutable-index.c b/src/search/dzl-fuzzy-mutable-index.c new file mode 100644 index 0000000..28ecdd0 --- /dev/null +++ b/src/search/dzl-fuzzy-mutable-index.c @@ -0,0 +1,641 @@ +/* dzl-fuzzy-mutable-index.c + * + * Copyright (C) 2014-2017 Christian Hergert + * + * 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 . + */ + +#include "config.h" + +#include +#include + +#include "search/dzl-fuzzy-mutable-index.h" + +/** + * SECTION:dzl-fuzzy-mutable-index + * @title: DzlFuzzyMutableIndex Matching + * @short_description: DzlFuzzyMutableIndex matching for GLib based programs. + * + * #DzlFuzzyMutableIndex provides a fulltext index that focuses around fuzzy + * matching words. This version of the datastructure is focused around + * in-memory storage. This makes mutability performance of adding or removing + * items from the corpus simpler. + * + * If you need mostly read-only indexes, you might consider using + * #DzlFuzzyIndex and #DzlFuzzyIndexBuilder which can create a disk-based file + * and mmap() a read-only version of the data set. + * + * It is a programming error to modify #Fuzzy while holding onto an array + * of #FuzzyMatch elements. The position of strings within the DzlFuzzyMutableIndexMatch + * may no longer be valid. + */ + +G_DEFINE_BOXED_TYPE (DzlFuzzyMutableIndex, dzl_fuzzy_mutable_index, + (GBoxedCopyFunc)dzl_fuzzy_mutable_index_ref, + (GBoxedFreeFunc)dzl_fuzzy_mutable_index_unref) + +struct _DzlFuzzyMutableIndex +{ + volatile gint ref_count; + GByteArray *heap; + GArray *id_to_text_offset; + GPtrArray *id_to_value; + GHashTable *char_tables; + GHashTable *removed; + guint in_bulk_insert : 1; + guint case_sensitive : 1; +}; + +#pragma pack(push, 1) +typedef struct +{ + guint id; + guint pos : 16; +} DzlFuzzyMutableIndexItem; +#pragma pack(pop) + +G_STATIC_ASSERT (sizeof(DzlFuzzyMutableIndexItem) == 6); + +typedef struct +{ + DzlFuzzyMutableIndex *fuzzy; + GArray **tables; + gint *state; + guint n_tables; + gsize max_matches; + const gchar *needle; + GHashTable *matches; +} DzlFuzzyMutableIndexLookup; + +static gint +dzl_fuzzy_mutable_index_item_compare (gconstpointer a, + gconstpointer b) +{ + gint ret; + + const DzlFuzzyMutableIndexItem *fa = a; + const DzlFuzzyMutableIndexItem *fb = b; + + if ((ret = fa->id - fb->id) == 0) + ret = fa->pos - fb->pos; + + return ret; +} + +static gint +dzl_fuzzy_mutable_index_match_compare (gconstpointer a, + gconstpointer b) +{ + const DzlFuzzyMutableIndexMatch *ma = a; + const DzlFuzzyMutableIndexMatch *mb = b; + + if (ma->score < mb->score) { + return 1; + } else if (ma->score > mb->score) { + return -1; + } + + return strcmp (ma->key, mb->key); +} + +DzlFuzzyMutableIndex * +dzl_fuzzy_mutable_index_ref (DzlFuzzyMutableIndex *fuzzy) +{ + g_return_val_if_fail (fuzzy, NULL); + g_return_val_if_fail (fuzzy->ref_count > 0, NULL); + + g_atomic_int_inc (&fuzzy->ref_count); + + return fuzzy; +} + +/** + * dzl_fuzzy_mutable_index_new: + * @case_sensitive: %TRUE if case should be preserved. + * + * Create a new #Fuzzy for fuzzy matching strings. + * + * Returns: A newly allocated #Fuzzy that should be freed with dzl_fuzzy_mutable_index_unref(). + */ +DzlFuzzyMutableIndex * +dzl_fuzzy_mutable_index_new (gboolean case_sensitive) +{ + DzlFuzzyMutableIndex *fuzzy; + + fuzzy = g_slice_new0 (DzlFuzzyMutableIndex); + fuzzy->ref_count = 1; + fuzzy->heap = g_byte_array_new (); + fuzzy->id_to_value = g_ptr_array_new (); + fuzzy->id_to_text_offset = g_array_new (FALSE, FALSE, sizeof (gsize)); + fuzzy->char_tables = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_array_unref); + fuzzy->case_sensitive = case_sensitive; + fuzzy->removed = g_hash_table_new (g_direct_hash, g_direct_equal); + + return fuzzy; +} + +DzlFuzzyMutableIndex * +dzl_fuzzy_mutable_index_new_with_free_func (gboolean case_sensitive, + GDestroyNotify free_func) +{ + DzlFuzzyMutableIndex *fuzzy; + + fuzzy = dzl_fuzzy_mutable_index_new (case_sensitive); + dzl_fuzzy_mutable_index_set_free_func (fuzzy, free_func); + + return fuzzy; +} + +void +dzl_fuzzy_mutable_index_set_free_func (DzlFuzzyMutableIndex *fuzzy, + GDestroyNotify free_func) +{ + g_return_if_fail (fuzzy); + + g_ptr_array_set_free_func (fuzzy->id_to_value, free_func); +} + +static gsize +dzl_fuzzy_mutable_index_heap_insert (DzlFuzzyMutableIndex *fuzzy, + const gchar *text) +{ + gsize ret; + + g_assert (fuzzy != NULL); + g_assert (text != NULL); + + ret = fuzzy->heap->len; + + g_byte_array_append (fuzzy->heap, (guint8 *)text, strlen (text) + 1); + + return ret; +} + +/** + * dzl_fuzzy_mutable_index_begin_bulk_insert: + * @fuzzy: (in): A #Fuzzy. + * + * Start a bulk insertion. @fuzzy is not ready for searching until + * dzl_fuzzy_mutable_index_end_bulk_insert() has been called. + * + * This allows for inserting large numbers of strings and deferring + * the final sort until dzl_fuzzy_mutable_index_end_bulk_insert(). + */ +void +dzl_fuzzy_mutable_index_begin_bulk_insert (DzlFuzzyMutableIndex *fuzzy) +{ + g_return_if_fail (fuzzy); + g_return_if_fail (!fuzzy->in_bulk_insert); + + fuzzy->in_bulk_insert = TRUE; +} + +/** + * dzl_fuzzy_mutable_index_end_bulk_insert: + * @fuzzy: (in): A #Fuzzy. + * + * Complete a bulk insert and resort the index. + */ +void +dzl_fuzzy_mutable_index_end_bulk_insert (DzlFuzzyMutableIndex *fuzzy) +{ + GHashTableIter iter; + gpointer key; + gpointer value; + + g_return_if_fail(fuzzy); + g_return_if_fail(fuzzy->in_bulk_insert); + + fuzzy->in_bulk_insert = FALSE; + + g_hash_table_iter_init (&iter, fuzzy->char_tables); + + while (g_hash_table_iter_next (&iter, &key, &value)) { + GArray *table = value; + + g_array_sort (table, dzl_fuzzy_mutable_index_item_compare); + } +} + +/** + * dzl_fuzzy_mutable_index_insert: + * @fuzzy: (in): A #Fuzzy. + * @key: (in): A UTF-8 encoded string. + * @value: (in): A value to associate with key. + * + * Inserts a string into the fuzzy matcher. + */ +void +dzl_fuzzy_mutable_index_insert (DzlFuzzyMutableIndex *fuzzy, + const gchar *key, + gpointer value) +{ + const gchar *tmp; + gchar *downcase = NULL; + gsize offset; + guint id; + + if (G_UNLIKELY (!key || !*key || (fuzzy->id_to_text_offset->len == G_MAXUINT))) + return; + + if (!fuzzy->case_sensitive) + downcase = g_utf8_casefold (key, -1); + + offset = dzl_fuzzy_mutable_index_heap_insert (fuzzy, key); + id = fuzzy->id_to_text_offset->len; + g_array_append_val (fuzzy->id_to_text_offset, offset); + g_ptr_array_add (fuzzy->id_to_value, value); + + if (!fuzzy->case_sensitive) + key = downcase; + + for (tmp = key; *tmp; tmp = g_utf8_next_char (tmp)) + { + gunichar ch = g_utf8_get_char (tmp); + GArray *table; + DzlFuzzyMutableIndexItem item; + + table = g_hash_table_lookup (fuzzy->char_tables, GINT_TO_POINTER (ch)); + + if (G_UNLIKELY (table == NULL)) + { + table = g_array_new (FALSE, FALSE, sizeof (DzlFuzzyMutableIndexItem)); + g_hash_table_insert (fuzzy->char_tables, GINT_TO_POINTER (ch), table); + } + + item.id = id; + item.pos = (guint)(gsize)(tmp - key); + + g_array_append_val (table, item); + } + + if (G_UNLIKELY (!fuzzy->in_bulk_insert)) + { + for (tmp = key; *tmp; tmp = g_utf8_next_char (tmp)) + { + GArray *table; + gunichar ch; + + ch = g_utf8_get_char (tmp); + table = g_hash_table_lookup (fuzzy->char_tables, GINT_TO_POINTER (ch)); + g_array_sort (table, dzl_fuzzy_mutable_index_item_compare); + } + } + + g_free (downcase); +} + +/** + * dzl_fuzzy_mutable_index_unref: + * @fuzzy: A #Fuzzy. + * + * Decrements the reference count of fuzzy by one. When the reference count + * reaches zero, the structure will be freed. + */ +void +dzl_fuzzy_mutable_index_unref (DzlFuzzyMutableIndex *fuzzy) +{ + g_return_if_fail (fuzzy); + g_return_if_fail (fuzzy->ref_count > 0); + + if (G_UNLIKELY (g_atomic_int_dec_and_test (&fuzzy->ref_count))) + { + g_byte_array_unref (fuzzy->heap); + fuzzy->heap = NULL; + + g_array_unref (fuzzy->id_to_text_offset); + fuzzy->id_to_text_offset = NULL; + + g_ptr_array_unref (fuzzy->id_to_value); + fuzzy->id_to_value = NULL; + + g_hash_table_unref (fuzzy->char_tables); + fuzzy->char_tables = NULL; + + g_hash_table_unref (fuzzy->removed); + fuzzy->removed = NULL; + + g_slice_free (DzlFuzzyMutableIndex, fuzzy); + } +} + +static gboolean +dzl_fuzzy_mutable_index_do_match (DzlFuzzyMutableIndexLookup *lookup, + DzlFuzzyMutableIndexItem *item, + guint table_index, + gint score) +{ + DzlFuzzyMutableIndexItem *iter; + gpointer key; + GArray *table; + gint *state; + gint iter_score; + + table = lookup->tables [table_index]; + state = &lookup->state [table_index]; + + for (; state [0] < (gint)table->len; state [0]++) + { + iter = &g_array_index (table, DzlFuzzyMutableIndexItem, state[0]); + + if ((iter->id < item->id) || ((iter->id == item->id) && (iter->pos <= item->pos))) + continue; + else if (iter->id > item->id) + break; + + iter_score = score + (iter->pos - item->pos); + + if ((table_index + 1) < lookup->n_tables) + { + if (dzl_fuzzy_mutable_index_do_match (lookup, iter, table_index + 1, iter_score)) + return TRUE; + continue; + } + + key = GINT_TO_POINTER (iter->id); + + if (!g_hash_table_contains (lookup->matches, key) || + (iter_score < GPOINTER_TO_INT (g_hash_table_lookup (lookup->matches, key)))) + g_hash_table_insert (lookup->matches, key, GINT_TO_POINTER (iter_score)); + + return TRUE; + } + + return FALSE; +} + +static inline const gchar * +dzl_fuzzy_mutable_index_get_string (DzlFuzzyMutableIndex *fuzzy, + gint id) +{ + guint offset = g_array_index (fuzzy->id_to_text_offset, gsize, id); + return (const gchar *)&fuzzy->heap->data [offset]; +} + +/** + * dzl_fuzzy_mutable_index_match: + * @fuzzy: (in): A #Fuzzy. + * @needle: (in): The needle to fuzzy search for. + * @max_matches: (in): The max number of matches to return. + * + * DzlFuzzyMutableIndex searches within @fuzzy for strings that fuzzy match @needle. + * Only up to @max_matches will be returned. + * + * TODO: max_matches is not yet respected. + * + * Returns: (transfer full) (element-type DzlFuzzyMutableIndexMatch): A newly allocated + * #GArray containing #FuzzyMatch elements. This should be freed when + * the caller is done with it using g_array_unref(). + * It is a programming error to keep the structure around longer than + * the @fuzzy instance. + */ +GArray * +dzl_fuzzy_mutable_index_match (DzlFuzzyMutableIndex *fuzzy, + const gchar *needle, + gsize max_matches) +{ + DzlFuzzyMutableIndexLookup lookup = { 0 }; + DzlFuzzyMutableIndexMatch match; + DzlFuzzyMutableIndexItem *item; + GHashTableIter iter; + gpointer key; + gpointer value; + const gchar *tmp; + GArray *matches = NULL; + GArray *root; + gchar *downcase = NULL; + guint i; + + g_return_val_if_fail (fuzzy, NULL); + g_return_val_if_fail (!fuzzy->in_bulk_insert, NULL); + g_return_val_if_fail (needle, NULL); + + matches = g_array_new (FALSE, FALSE, sizeof (DzlFuzzyMutableIndexMatch)); + + if (!*needle) + goto cleanup; + + if (!fuzzy->case_sensitive) + { + downcase = g_utf8_casefold (needle, -1); + needle = downcase; + } + + lookup.fuzzy = fuzzy; + lookup.n_tables = g_utf8_strlen (needle, -1); + lookup.state = g_new0 (gint, lookup.n_tables); + lookup.tables = g_new0 (GArray*, lookup.n_tables); + lookup.needle = needle; + lookup.max_matches = max_matches; + lookup.matches = g_hash_table_new (NULL, NULL); + + for (i = 0, tmp = needle; *tmp; tmp = g_utf8_next_char (tmp)) + { + gunichar ch; + GArray *table; + + ch = g_utf8_get_char (tmp); + table = g_hash_table_lookup (fuzzy->char_tables, GINT_TO_POINTER (ch)); + + if (table == NULL) + goto cleanup; + + lookup.tables [i++] = table; + } + + g_assert (lookup.n_tables == i); + g_assert (lookup.tables [0] != NULL); + + root = lookup.tables [0]; + + if (G_LIKELY (lookup.n_tables > 1)) + { + for (i = 0; i < root->len; i++) + { + item = &g_array_index (root, DzlFuzzyMutableIndexItem, i); + dzl_fuzzy_mutable_index_do_match (&lookup, item, 1, MIN (16, item->pos * 4)); + } + } + else + { + guint last_id = G_MAXUINT; + + for (i = 0; i < root->len; i++) + { + item = &g_array_index (root, DzlFuzzyMutableIndexItem, i); + match.id = GPOINTER_TO_INT (item->id); + if (match.id != last_id) + { + match.key = dzl_fuzzy_mutable_index_get_string (fuzzy, item->id); + match.value = g_ptr_array_index (fuzzy->id_to_value, item->id); + match.score = 1.0 / (strlen (match.key) + item->pos); + g_array_append_val (matches, match); + last_id = match.id; + } + } + + goto cleanup; + } + + g_hash_table_iter_init (&iter, lookup.matches); + + while (g_hash_table_iter_next (&iter, &key, &value)) + { + /* Ignore keys that have a tombstone record. */ + if (g_hash_table_contains (fuzzy->removed, key)) + continue; + + match.id = GPOINTER_TO_INT (key); + match.key = dzl_fuzzy_mutable_index_get_string (fuzzy, match.id); + match.score = 1.0 / (strlen (match.key) + GPOINTER_TO_INT (value)); + match.value = g_ptr_array_index (fuzzy->id_to_value, match.id); + + g_array_append_val (matches, match); + } + + /* + * TODO: We could be more clever here when inserting into the array + * only if it is a lower score than the end or < max items. + */ + + if (max_matches != 0) + { + g_array_sort (matches, dzl_fuzzy_mutable_index_match_compare); + + if (max_matches && (matches->len > max_matches)) + g_array_set_size (matches, max_matches); + } + +cleanup: + g_free (downcase); + g_free (lookup.state); + g_free (lookup.tables); + g_clear_pointer (&lookup.matches, g_hash_table_unref); + + return matches; +} + +gboolean +dzl_fuzzy_mutable_index_contains (DzlFuzzyMutableIndex *fuzzy, + const gchar *key) +{ + GArray *ar; + gboolean ret; + + g_return_val_if_fail (fuzzy != NULL, FALSE); + + ar = dzl_fuzzy_mutable_index_match (fuzzy, key, 1); + ret = (ar != NULL) && (ar->len > 0); + g_clear_pointer (&ar, g_array_unref); + + return ret; +} + +void +dzl_fuzzy_mutable_index_remove (DzlFuzzyMutableIndex *fuzzy, + const gchar *key) +{ + GArray *ar; + + g_return_if_fail (fuzzy != NULL); + + if (!key || !*key) + return; + + ar = dzl_fuzzy_mutable_index_match (fuzzy, key, 1); + + if (ar != NULL && ar->len > 0) + { + for (guint i = 0; i < ar->len; i++) + { + const DzlFuzzyMutableIndexMatch *match = &g_array_index (ar, DzlFuzzyMutableIndexMatch, i); + + if (g_strcmp0 (match->key, key) == 0) + g_hash_table_insert (fuzzy->removed, GINT_TO_POINTER (match->id), NULL); + } + } + + g_clear_pointer (&ar, g_array_unref); +} + +gchar * +dzl_fuzzy_highlight (const gchar *str, + const gchar *match, + gboolean case_sensitive) +{ + static const gchar *begin = ""; + static const gchar *end = ""; + GString *ret; + gunichar str_ch; + gunichar match_ch; + gboolean element_open = FALSE; + + if (str == NULL || match == NULL) + return g_strdup (str); + + ret = g_string_new (NULL); + + for (; *str; str = g_utf8_next_char (str)) + { + str_ch = g_utf8_get_char (str); + match_ch = g_utf8_get_char (match); + + if (str_ch == '&') + { + if (0 == strncmp (str, "&", 5)) + { + str += 4; + g_string_append (ret, "&"); + continue; + } + else if (0 == strncmp (str, "'", 6)) + { + str += 5; + g_string_append (ret, "'"); + continue; + } + } + + if ((str_ch == match_ch) || + (!case_sensitive && g_unichar_tolower (str_ch) == g_unichar_tolower (match_ch))) + { + if (!element_open) + { + g_string_append (ret, begin); + element_open = TRUE; + } + + g_string_append_unichar (ret, str_ch); + + /* TODO: We could seek to the next char and append in a batch. */ + match = g_utf8_next_char (match); + } + else + { + if (element_open) + { + g_string_append (ret, end); + element_open = FALSE; + } + + g_string_append_unichar (ret, str_ch); + } + } + + if (element_open) + g_string_append (ret, end); + + return g_string_free (ret, FALSE); +} diff --git a/src/search/dzl-fuzzy-mutable-index.h b/src/search/dzl-fuzzy-mutable-index.h new file mode 100644 index 0000000..ed25f8a --- /dev/null +++ b/src/search/dzl-fuzzy-mutable-index.h @@ -0,0 +1,79 @@ +/* fuzzy.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_FUZZY_MUTABLE_INDEX_H +#define DZL_FUZZY_MUTABLE_INDEX_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_FUZZY_MUTABLE_INDEX (dzl_fuzzy_mutable_index_get_type()) + +typedef struct _DzlFuzzyMutableIndex DzlFuzzyMutableIndex; + +typedef struct +{ + const gchar *key; + gpointer value; + gfloat score; + guint id; +} DzlFuzzyMutableIndexMatch; + +DZL_AVAILABLE_IN_ALL +GType dzl_fuzzy_mutable_index_get_type (void); +DZL_AVAILABLE_IN_ALL +DzlFuzzyMutableIndex *dzl_fuzzy_mutable_index_new (gboolean case_sensitive); +DZL_AVAILABLE_IN_ALL +DzlFuzzyMutableIndex *dzl_fuzzy_mutable_index_new_with_free_func (gboolean case_sensitive, + GDestroyNotify free_func); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_mutable_index_set_free_func (DzlFuzzyMutableIndex *fuzzy, + GDestroyNotify free_func); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_mutable_index_begin_bulk_insert (DzlFuzzyMutableIndex *fuzzy); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_mutable_index_end_bulk_insert (DzlFuzzyMutableIndex *fuzzy); +DZL_AVAILABLE_IN_ALL +gboolean dzl_fuzzy_mutable_index_contains (DzlFuzzyMutableIndex *fuzzy, + const gchar *key); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_mutable_index_insert (DzlFuzzyMutableIndex *fuzzy, + const gchar *key, + gpointer value); +DZL_AVAILABLE_IN_ALL +GArray *dzl_fuzzy_mutable_index_match (DzlFuzzyMutableIndex *fuzzy, + const gchar *needle, + gsize max_matches); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_mutable_index_remove (DzlFuzzyMutableIndex *fuzzy, + const gchar *key); +DZL_AVAILABLE_IN_ALL +DzlFuzzyMutableIndex *dzl_fuzzy_mutable_index_ref (DzlFuzzyMutableIndex *fuzzy); +DZL_AVAILABLE_IN_ALL +void dzl_fuzzy_mutable_index_unref (DzlFuzzyMutableIndex *fuzzy); +DZL_AVAILABLE_IN_ALL +gchar *dzl_fuzzy_highlight (const gchar *str, + const gchar *query, + gboolean case_sensitive); + +G_END_DECLS + +#endif /* DZL_FUZZY_MUTABLE_INDEX_H */ diff --git a/src/search/dzl-levenshtein.c b/src/search/dzl-levenshtein.c new file mode 100644 index 0000000..3f3b7b6 --- /dev/null +++ b/src/search/dzl-levenshtein.c @@ -0,0 +1,109 @@ +/* dzl-levenshtein.c + * + * Copyright (C) 2013-2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or (at + * your option) any later version. + * + * This file 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 Lesser 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 . + */ + +#include "config.h" + +#include +#include + +#include "dzl-levenshtein.h" + +gint +dzl_levenshtein (const gchar *needle, + const gchar *haystack) +{ + const gchar *s; + const gchar *t; + gunichar sc; + gunichar tc; + gint *v0; + gint *v1; + gint haystack_char_len; + gint cost; + gint i; + gint j; + gint ret; + + g_return_val_if_fail (needle, G_MAXINT); + g_return_val_if_fail (haystack, G_MAXINT); + + /* + * Handle degenerate cases. + */ + if (!g_strcmp0(needle, haystack)) { + return 0; + } else if (!*needle) { + return g_utf8_strlen(haystack, -1); + } else if (!*haystack) { + return g_utf8_strlen(needle, -1); + } + + haystack_char_len = g_utf8_strlen (haystack, -1); + + /* + * Create two vectors to hold our states. + */ + v0 = g_new0(int, haystack_char_len + 1); + v1 = g_new0(int, haystack_char_len + 1); + + /* + * initialize v0 (the previous row of distances). + * this row is A[0][i]: edit distance for an empty needle. + * the distance is just the number of characters to delete from haystack. + */ + for (i = 0; i < haystack_char_len + 1; i++) { + v0[i] = i; + } + + for (i = 0, s = needle; s && *s; i++, s = g_utf8_next_char(s)) { + /* + * Calculate v1 (current row distances) from the previous row v0. + */ + + sc = g_utf8_get_char(s); + + /* + * first element of v1 is A[i+1][0] + * + * edit distance is delete (i+1) chars from needle to match empty + * haystack. + */ + v1[0] = i + 1; + + /* + * use formula to fill in the rest of the row. + */ + for (j = 0, t = haystack; t && *t; j++, t = g_utf8_next_char(t)) { + tc = g_utf8_get_char(t); + cost = (sc == tc) ? 0 : 1; + v1[j+1] = MIN(v1[j] + 1, MIN(v0[j+1] + 1, v0[j] + cost)); + } + + /* + * copy v1 (current row) to v0 (previous row) for next iteration. + */ + memcpy(v0, v1, sizeof(gint) * haystack_char_len); + } + + ret = v1[haystack_char_len]; + + g_free(v0); + g_free(v1); + + return ret; +} diff --git a/src/search/dzl-levenshtein.h b/src/search/dzl-levenshtein.h new file mode 100644 index 0000000..7a62cf9 --- /dev/null +++ b/src/search/dzl-levenshtein.h @@ -0,0 +1,34 @@ +/* dzl-levenshtein.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_LEVENSHTEIN_H +#define DZL_LEVENSHTEIN_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +gint dzl_levenshtein (const gchar *needle, + const gchar *haystack); + +G_END_DECLS + +#endif /* DZL_LEVENSHTEIN_H */ diff --git a/src/search/dzl-pattern-spec.c b/src/search/dzl-pattern-spec.c new file mode 100644 index 0000000..4cde734 --- /dev/null +++ b/src/search/dzl-pattern-spec.c @@ -0,0 +1,206 @@ +/* dzl-pattern-spec.c + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-pattern-spec" + +#include "config.h" + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif +#include + +#include "dzl-pattern-spec.h" + +G_DEFINE_BOXED_TYPE (DzlPatternSpec, dzl_pattern_spec, dzl_pattern_spec_ref, dzl_pattern_spec_unref) + +/** + * SECTION:dzl-pattern-spec + * @title: DzlPatternSpec + * @short_description: Simple glob-like searching + * + * This works similar to #GPatternSpec except the query syntax is different. + * It tries to mtach word boundaries, but with matching partial words up + * to those boundaries. For example, "gtk widg" would match "gtk_widget_show". + * Word boundaries include '_' and ' '. If any character is uppercase, then + * case sensitivity is used. + */ + +struct _DzlPatternSpec +{ + volatile gint ref_count; + gchar *needle; + gchar **parts; + guint case_sensitive : 1; +}; + +#ifdef G_OS_WIN32 +/* A fallback for missing strcasestr() on Windows. This is not in any way + * optimized, but at least it supports something resembling UTF-8. + */ +static char * +strcasestr (const gchar *haystack, + const gchar *needle) +{ + g_autofree gchar *haystack_folded = g_utf8_casefold (haystack, -1); + g_autofree gchar *needle_folded = g_utf8_casefold (needle, -1); + const gchar *pos; + gsize n_chars = 0; + + pos = strstr (haystack_folded, needle_folded); + + if (pos == NULL) + return NULL; + + for (const gchar *iter = haystack_folded; + *iter != '\0'; + iter = g_utf8_next_char (iter)) + { + if (iter >= pos) + break; + n_chars++; + } + + return g_utf8_offset_to_pointer (haystack, n_chars); +} +#endif + +DzlPatternSpec * +dzl_pattern_spec_new (const gchar *needle) +{ + DzlPatternSpec *self; + const gchar *tmp; + + if (needle == NULL) + needle = ""; + + self = g_slice_new0 (DzlPatternSpec); + self->ref_count = 1; + self->needle = g_strdup (needle); + self->parts = g_strsplit (needle, " ", 0); + self->case_sensitive = FALSE; + + for (tmp = needle; *tmp; tmp = g_utf8_next_char (tmp)) + { + if (g_unichar_isupper (g_utf8_get_char (tmp))) + { + self->case_sensitive = TRUE; + break; + } + } + + return self; +} + +const gchar * +dzl_pattern_spec_get_text (DzlPatternSpec *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->needle; +} + +static void +dzl_pattern_spec_free (DzlPatternSpec *self) +{ + g_clear_pointer (&self->parts, g_strfreev); + g_clear_pointer (&self->needle, g_free); + g_slice_free (DzlPatternSpec, self); +} + +static inline gboolean +is_word_break (gunichar ch) +{ + return (ch == ' ' || ch == '_' || ch == '-'); +} + +static const gchar * +next_word_start (const gchar *haystack) +{ + for (; *haystack; haystack = g_utf8_next_char (haystack)) + { + gunichar ch = g_utf8_get_char (haystack); + + if (is_word_break (ch)) + break; + } + + for (; *haystack; haystack = g_utf8_next_char (haystack)) + { + gunichar ch = g_utf8_get_char (haystack); + + if (is_word_break (ch)) + continue; + + break; + } + + g_return_val_if_fail (*haystack == '\0' || !is_word_break (*haystack), NULL); + + return haystack; +} + +gboolean +dzl_pattern_spec_match (DzlPatternSpec *self, + const gchar *haystack) +{ + gsize i; + + if (self == NULL || haystack == NULL) + return FALSE; + + for (i = 0; (haystack != NULL) && self->parts [i]; i++) + { + if (self->parts [i][0] == '\0') + continue; + + if (self->case_sensitive) + haystack = strstr (haystack, self->parts [i]); + else + haystack = strcasestr (haystack, self->parts [i]); + + if (haystack == NULL) + return FALSE; + + if (self->parts [i + 1] != NULL) + haystack = next_word_start (haystack + strlen (self->parts [i])); + } + + return TRUE; +} + +DzlPatternSpec * +dzl_pattern_spec_ref (DzlPatternSpec *self) +{ + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (self->ref_count > 0, NULL); + + g_atomic_int_inc (&self->ref_count); + + return self; +} + +void +dzl_pattern_spec_unref (DzlPatternSpec *self) +{ + g_return_if_fail (self); + g_return_if_fail (self->ref_count > 0); + + if (g_atomic_int_dec_and_test (&self->ref_count)) + dzl_pattern_spec_free (self); +} diff --git a/src/search/dzl-pattern-spec.h b/src/search/dzl-pattern-spec.h new file mode 100644 index 0000000..d783a6d --- /dev/null +++ b/src/search/dzl-pattern-spec.h @@ -0,0 +1,50 @@ +/* dzl-pattern-spec.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_MATCH_PATTERN_H +#define DZL_MATCH_PATTERN_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +typedef struct _DzlPatternSpec DzlPatternSpec; + +#define DZL_TYPE_PATTERN_SPEC (dzl_pattern_spec_get_type()) + +DZL_AVAILABLE_IN_ALL +GType dzl_pattern_spec_get_type (void); +DZL_AVAILABLE_IN_ALL +DzlPatternSpec *dzl_pattern_spec_new (const gchar *keywords); +DZL_AVAILABLE_IN_ALL +DzlPatternSpec *dzl_pattern_spec_ref (DzlPatternSpec *self); +DZL_AVAILABLE_IN_ALL +void dzl_pattern_spec_unref (DzlPatternSpec *self); +DZL_AVAILABLE_IN_ALL +gboolean dzl_pattern_spec_match (DzlPatternSpec *self, + const gchar *haystack); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_pattern_spec_get_text (DzlPatternSpec *self); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (DzlPatternSpec, dzl_pattern_spec_unref) + +G_END_DECLS + +#endif /* DZL_MATCH_PATTERN_H */ diff --git a/src/search/dzl-trie.c b/src/search/dzl-trie.c new file mode 100644 index 0000000..dde0940 --- /dev/null +++ b/src/search/dzl-trie.c @@ -0,0 +1,829 @@ +/* dzl-trie.c + * + * Copyright (C) 2012 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#include "config.h" + +#include + +#include + +#include "search/dzl-trie.h" +#include "util/dzl-macros.h" + +/** + * SECTION:trie + * @title: DzlTrie + * @short_description: A generic prefix tree. + * + * The #DzlTrie struct and its associated functions provide a DzlTrie data structure, + * where nodes in the tree can contain arbitrary data. + * + * To create a new #DzlTrie use dzl_trie_new(). You can free it with dzl_trie_free(). + * To insert a key and value pair into the #DzlTrie use dzl_trie_insert(). + * To remove a key from the #DzlTrie use dzl_trie_remove(). + * To traverse all children of the #DzlTrie from a given key use dzl_trie_traverse(). + */ + +typedef struct _DzlTrieNode DzlTrieNode; +typedef struct _DzlTrieNodeChunk DzlTrieNodeChunk; + +G_DEFINE_BOXED_TYPE (DzlTrie, dzl_trie, dzl_trie_ref, dzl_trie_unref) + +/* + * Try to optimize for 64 bit vs 32 bit pointers. We are assuming that the + * 32 bit also has a 32 byte cacheline. This is very likely not the case + * everwhere, such as x86_64 compiled with -m32. However, accomidating that + * will require some changes to the layout of each chunk. + */ +#if GLIB_SIZEOF_VOID_P == 8 +# define FIRST_CHUNK_KEYS 4 +# define TRIE_NODE_SIZE 64 +# define TRIE_NODE_CHUNK_SIZE 64 +# define TRIE_NODE_CHUNK_KEYS(c) (((c)->is_inline) ? 4 : 6) +#else +# define FIRST_CHUNK_KEYS 3 +# define TRIE_NODE_SIZE 32 +# define TRIE_NODE_CHUNK_SIZE 32 +# define TRIE_NODE_CHUNK_KEYS(c) (((c)->is_inline) ? 3 : 5) +#endif + +/** + * DzlTrieNodeChunk: + * @flags: Flags describing behaviors of the DzlTrieNodeChunk. + * @count: The number of items added to this chunk. + * @keys: The keys for @children. + * @next: The next #DzlTrieNodeChunk if there is one. + * @children: The children #DzlTrieNodeChunk or %NULL. If the chunk is + * inline the DzlTrieNode, then there will be fewer items. + */ +DZL_ALIGNED_BEGIN(1) +struct _DzlTrieNodeChunk +{ + DzlTrieNodeChunk *next; + gboolean is_inline : 1; + guint flags : 7; + guint count : 8; + guint8 keys[6]; + DzlTrieNode *children[0]; +} DZL_ALIGNED_END(1); + +/** + * DzlTrieNode: + * @parent: The parent #DzlTrieNode. When a node is destroyed, it may need to + * walk up to the parent node and unlink itself. + * @value: A pointer to the user provided value, or %NULL. + * @chunk: The first chunk in the chain. Inline chunks have fewer children + * elements than extra allocated chunks so that they are cache aligned. + */ +DZL_ALIGNED_BEGIN(1) +struct _DzlTrieNode +{ + DzlTrieNode *parent; + gpointer value; + DzlTrieNodeChunk chunk; +} DZL_ALIGNED_END(1); + +/** + * DzlTrie: + * @value_destroy: A #GDestroyNotify to free data pointers. + * @root: The root DzlTrieNode. + */ +struct _DzlTrie +{ + volatile gint ref_count; + GDestroyNotify value_destroy; + DzlTrieNode *root; +}; + +#if GLIB_SIZEOF_VOID_P == 8 + G_STATIC_ASSERT (sizeof(gpointer) == 8); + G_STATIC_ASSERT ((FIRST_CHUNK_KEYS-1) == 3); + G_STATIC_ASSERT (((FIRST_CHUNK_KEYS-1) * sizeof(gpointer)) == 24); + G_STATIC_ASSERT(sizeof(DzlTrieNode) == 32); + G_STATIC_ASSERT(sizeof(DzlTrieNodeChunk) == 16); +#else + G_STATIC_ASSERT (sizeof(gpointer) == 4); + G_STATIC_ASSERT ((FIRST_CHUNK_KEYS-1) == 2); + G_STATIC_ASSERT (((FIRST_CHUNK_KEYS-1) * sizeof(gpointer)) == 8); + G_STATIC_ASSERT(sizeof(DzlTrieNode) == 20); + G_STATIC_ASSERT(sizeof(DzlTrieNodeChunk) == 12); +#endif + +/** + * dzl_trie_malloc0: + * @trie: A #DzlTrie + * @size: Number of bytes to allocate. + * + * Wrapper function to allocate a memory chunk. This gives us somewhere to + * perform the abstraction if we want to use mmap()'d I/O at some point. + * The memory will be zero'd before being returned. + * + * Returns: A pointer to the allocation. + */ +static gpointer +dzl_trie_malloc0 (DzlTrie *trie, + gsize size) +{ + return g_malloc0(size); +} + +/** + * dzl_trie_free: + * @trie: A #DzlTrie. + * @data: The data to free. + * + * Frees a portion of memory allocated by @trie. + */ +static void +dzl_trie_free (DzlTrie *trie, + gpointer data) +{ + g_free(data); +} + +/** + * dzl_trie_node_new: + * @trie: A #DzlTrie. + * @parent: The nodes parent or %NULL. + * + * Create a new node that can be placed in a DzlTrie. The node contains a chunk + * embedded in it that may contain only 4 pointers instead of the full 6 do + * to the overhead of the DzlTrieNode itself. + * + * Returns: A newly allocated DzlTrieNode that should be freed with g_free(). + */ +DzlTrieNode * +dzl_trie_node_new (DzlTrie *trie, + DzlTrieNode *parent) +{ + DzlTrieNode *node; + + node = dzl_trie_malloc0(trie, TRIE_NODE_SIZE); + node->chunk.is_inline = TRUE; + node->parent = parent; + return node; +} + +/** + * dzl_trie_node_chunk_is_full: + * @chunk: A #DzlTrieNodeChunk. + * + * Checks to see if all children slots are full in @chunk. + * + * Returns: %TRUE if there are no free slots in @chunk. + */ +G_INLINE_FUNC gboolean +dzl_trie_node_chunk_is_full (DzlTrieNodeChunk *chunk) +{ + g_assert(chunk); + return (chunk->count == TRIE_NODE_CHUNK_KEYS(chunk)); +} + +/** + * dzl_trie_node_chunk_new: + * @trie: The DzlTrie that the chunk belongs to. + * + * Creates a new #DzlTrieNodeChunk with empty state. + * + * Returns: (transfer full): A #DzlTrieNodeChunk. + */ +DzlTrieNodeChunk * +dzl_trie_node_chunk_new (DzlTrie *trie) +{ + return dzl_trie_malloc0(trie, TRIE_NODE_CHUNK_SIZE); +} + +/** + * dzl_trie_append_to_node: + * @chunk: A #DzlTrieNodeChunk. + * @key: The key to append. + * @child: A #DzlTrieNode to append. + * + * Appends @child to the chunk. If there is not room in the chunk, + * then a new chunk will be added and append to @chunk. + * + * @chunk MUST be the last chunk in the chain (therefore chunk->next + * is %NULL). + */ +static void +dzl_trie_append_to_node (DzlTrie *trie, + DzlTrieNode *node, + DzlTrieNodeChunk *chunk, + guint8 key, + DzlTrieNode *child) +{ + g_assert(trie); + g_assert(node); + g_assert(chunk); + g_assert(child); + + if (dzl_trie_node_chunk_is_full(chunk)) { + chunk->next = dzl_trie_node_chunk_new(trie); + chunk = chunk->next; + } + + chunk->keys[chunk->count] = key; + chunk->children[chunk->count] = child; + chunk->count++; +} + +/** + * dzl_trie_node_move_to_front: + * @node: A #DzlTrieNode. + * @chunk: A #DzlTrieNodeChunk. + * @idx: The index of the item to move. + * + * Moves the key and child found at @idx to the beginning of the first chunk + * to achieve better cache locality. + */ +static void +dzl_trie_node_move_to_front (DzlTrieNode *node, + DzlTrieNodeChunk *chunk, + guint idx) +{ + DzlTrieNodeChunk *first; + DzlTrieNode *child; + guint8 offset; + guint8 key; + + g_assert(node); + g_assert(chunk); + + first = &node->chunk; + + key = chunk->keys[idx]; + child = chunk->children[idx]; + + offset = ((first == chunk) ? first->count : FIRST_CHUNK_KEYS) - 1; + chunk->keys[idx] = first->keys[offset]; + chunk->children[idx] = first->children[offset]; + + memmove(&first->keys[1], &first->keys[0], (FIRST_CHUNK_KEYS-1)); + memmove(&first->children[1], &first->children[0], (FIRST_CHUNK_KEYS-1) * sizeof(DzlTrieNode *)); + + first->keys[0] = key; + first->children[0] = child; +} + +/** + * dzl_trie_find_node: + * @trie: The #DzlTrie we are searching. + * @node: A #DzlTrieNode. + * @key: The key to find in this node. + * + * Searches the chunk chain of the current node for the key provided. If + * found, the child node for that key is returned. + * + * Returns: (transfer none): A #DzlTrieNode or %NULL. + */ +static DzlTrieNode * +dzl_trie_find_node (DzlTrie *trie, + DzlTrieNode *node, + guint8 key) +{ + DzlTrieNodeChunk *iter; + guint i; + + g_assert(node); + + for (iter = &node->chunk; iter; iter = iter->next) { + for (i = 0; i < iter->count; i++) { + if (iter->keys[i] == key) { + if (iter != &node->chunk) { + dzl_trie_node_move_to_front(node, iter, i); + __builtin_prefetch(node->chunk.children[0]); + return node->chunk.children[0]; + } + __builtin_prefetch(iter->children[i]); + return iter->children[i]; + } + } + } + + return NULL; +} + +/** + * dzl_trie_find_or_create_node: + * @trie: A #DzlTrie. + * @node: A #DzlTrieNode. + * @key: The key to insert. + * + * Attempts to find key within @node. If @key is not found, it is added to the + * node. The child for the key is returned. + * + * Returns: (transfer none): The child #DzlTrieNode for @key. + */ +static DzlTrieNode * +dzl_trie_find_or_create_node (DzlTrie *trie, + DzlTrieNode *node, + guint8 key) +{ + DzlTrieNodeChunk *iter; + DzlTrieNodeChunk *last = NULL; + guint i; + + g_assert(node); + + for (iter = &node->chunk; iter; iter = iter->next) { + for (i = 0; i < iter->count; i++) { + if (iter->keys[i] == key) { + if (iter != &node->chunk) { + dzl_trie_node_move_to_front(node, iter, i); + __builtin_prefetch(node->chunk.children[0]); + return node->chunk.children[0]; + } + __builtin_prefetch(iter->children[i]); + return iter->children[i]; + } + } + last = iter; + } + + g_assert(last); + + node = dzl_trie_node_new(trie, node); + dzl_trie_append_to_node(trie, node->parent, last, key, node); + return node; +} + +/** + * dzl_trie_node_remove_fast: + * @node: A #DzlTrieNode. + * @chunk: A #DzlTrieNodeChunk. + * @idx: The child within the chunk. + * + * Removes child at index @idx from the chunk. The last item in the + * chain of chunks will be moved to the slot indicated by @idx. + */ +G_INLINE_FUNC void +dzl_trie_node_remove_fast (DzlTrieNode *node, + DzlTrieNodeChunk *chunk, + guint idx) +{ + DzlTrieNodeChunk *iter; + + g_assert(node); + g_assert(chunk); + + for (iter = chunk; iter->next && iter->next->count; iter = iter->next) { } + + g_assert(iter->count); + + chunk->keys[idx] = iter->keys[iter->count-1]; + chunk->children[idx] = iter->children[iter->count-1]; + + iter->count--; + + iter->keys[iter->count] = '\0'; + iter->children[iter->count] = NULL; +} + +/** + * dzl_trie_node_unlink: + * @node: A #DzlTrieNode. + * + * Unlinks @node from the DzlTrie. The parent node has its pointer to @node + * removed. + */ +static void +dzl_trie_node_unlink (DzlTrieNode *node) +{ + DzlTrieNodeChunk *iter; + DzlTrieNode *parent; + guint i; + + g_assert(node); + + if ((parent = node->parent)) { + node->parent = NULL; + for (iter = &parent->chunk; iter; iter = iter->next) { + for (i = 0; i < iter->count; i++) { + if (iter->children[i] == node) { + dzl_trie_node_remove_fast(node, iter, i); + g_assert(iter->children[i] != node); + return; + } + } + } + } +} + +/** + * dzl_trie_destroy_node: + * @trie: A #DzlTrie. + * @node: A #DzlTrieNode. + * @value_destroy: A #GDestroyNotify or %NULL. + * + * Removes @node from the #DzlTrie and releases all memory associated with it. + * If the nodes value is set, @value_destroy will be called to release it. + * + * The reclaimation happens as such: + * + * 1) the node is unlinked from its parent. + * 2) each of the children are destroyed, leaving us an empty chain. + * 3) each of the allocated chain links are freed. + * 4) the value pointer is freed. + * 5) the structure itself is freed. + */ +static void +dzl_trie_destroy_node (DzlTrie *trie, + DzlTrieNode *node, + GDestroyNotify value_destroy) +{ + DzlTrieNodeChunk *iter; + DzlTrieNodeChunk *tmp; + + g_assert(node); + + dzl_trie_node_unlink(node); + + while (node->chunk.count) { + dzl_trie_destroy_node(trie, node->chunk.children[0], value_destroy); + } + + for (iter = node->chunk.next; iter;) { + tmp = iter; + iter = iter->next; + dzl_trie_free(trie, tmp); + } + + if (node->value && value_destroy) { + value_destroy(node->value); + } + + dzl_trie_free(trie, node); +} + +/** + * dzl_trie_new: + * @value_destroy: A #GDestroyNotify, or %NULL. + * + * Creates a new #DzlTrie. When a value is removed from the trie, @value_destroy + * will be called to allow you to release any resources. + * + * Returns: (transfer full): A newly allocated #DzlTrie that should be freed + * with dzl_trie_unref(). + */ +DzlTrie * +dzl_trie_new (GDestroyNotify value_destroy) +{ + DzlTrie *trie; + + trie = g_new0(DzlTrie, 1); + trie->ref_count = 1; + trie->root = dzl_trie_node_new(trie, NULL); + trie->value_destroy = value_destroy; + + return trie; +} + +/** + * dzl_trie_insert: + * @trie: A #DzlTrie. + * @key: The key to insert. + * @value: The value to insert. + * + * Inserts @value into @trie located with @key. + */ +void +dzl_trie_insert (DzlTrie *trie, + const gchar *key, + gpointer value) +{ + DzlTrieNode *node; + + g_return_if_fail(trie); + g_return_if_fail(key); + g_return_if_fail(value); + + node = trie->root; + + while (*key) { + node = dzl_trie_find_or_create_node(trie, node, *key); + key++; + } + + if (node->value && trie->value_destroy) { + trie->value_destroy(node->value); + } + + node->value = value; +} + +/** + * dzl_trie_lookup: + * @trie: A #DzlTrie. + * @key: The key to lookup. + * + * Looks up @key in @trie and returns the value associated. + * + * Returns: (transfer none): The value inserted or %NULL. + */ +gpointer +dzl_trie_lookup (DzlTrie *trie, + const gchar *key) +{ + DzlTrieNode *node; + + __builtin_prefetch(trie); + __builtin_prefetch(key); + + g_return_val_if_fail(trie, NULL); + g_return_val_if_fail(key, NULL); + + node = trie->root; + + while (*key && node) { + node = dzl_trie_find_node(trie, node, *key); + key++; + } + + return node ? node->value : NULL; +} + +/** + * dzl_trie_remove: + * @trie: A #DzlTrie. + * @key: The key to remove. + * + * Removes @key from @trie, possibly destroying the value associated with + * the key. + * + * Returns: %TRUE if @key was found, otherwise %FALSE. + */ +gboolean +dzl_trie_remove (DzlTrie *trie, + const gchar *key) +{ + DzlTrieNode *node; + + g_return_val_if_fail(trie, FALSE); + g_return_val_if_fail(key, FALSE); + + node = trie->root; + + while (*key && node) { + node = dzl_trie_find_node(trie, node, *key); + key++; + } + + if (node && node->value) { + if (trie->value_destroy) { + trie->value_destroy(node->value); + } + + node->value = NULL; + + if (!node->chunk.count) { + while (node->parent && + node->parent->parent && + !node->parent->value && + (node->parent->chunk.count == 1)) { + node = node->parent; + } + dzl_trie_destroy_node(trie, node, trie->value_destroy); + } + + return TRUE; + } + + return FALSE; +} + +/** + * dzl_trie_traverse_node_pre_order: + * @trie: A #DzlTrie. + * @node: A #DzlTrieNode. + * @str: The prefix for this node. + * @flags: The flags for which nodes to callback. + * @max_depth: the maximum depth to process. + * @func: (scope call) (closure user_data): The func to execute for each matching node. + * @user_data: User data for @func. + * + * Traverses node and all of its children according to the parameters + * provided. @func is called for each matching node. + * + * This assumes that the order is %G_POST_ORDER, and therefore does not + * have the conditionals to check pre-vs-pre ordering. + * + * Returns: %TRUE if traversal was cancelled; otherwise %FALSE. + */ +static gboolean +dzl_trie_traverse_node_pre_order (DzlTrie *trie, + DzlTrieNode *node, + GString *str, + GTraverseFlags flags, + gint max_depth, + DzlTrieTraverseFunc func, + gpointer user_data) +{ + DzlTrieNodeChunk *iter; + gboolean ret = FALSE; + guint i; + + g_assert(trie); + g_assert(node); + g_assert(str); + + if (max_depth) { + if ((!node->value && (flags & G_TRAVERSE_NON_LEAVES)) || + (node->value && (flags & G_TRAVERSE_LEAVES))) { + if (func(trie, str->str, node->value, user_data)) { + return TRUE; + } + } + for (iter = &node->chunk; iter; iter = iter->next) { + for (i = 0; i < iter->count; i++) { + g_string_append_c(str, iter->keys[i]); + if (dzl_trie_traverse_node_pre_order(trie, + iter->children[i], + str, + flags, + max_depth - 1, + func, + user_data)) { + return TRUE; + } + g_string_truncate(str, str->len - 1); + } + } + } + + return ret; +} + +/** + * dzl_trie_traverse_node_post_order: + * @trie: A #DzlTrie. + * @node: A #DzlTrieNode. + * @str: The prefix for this node. + * @flags: The flags for which nodes to callback. + * @max_depth: the maximum depth to process. + * @func: (scope call) (closure user_data): The func to execute for each matching node. + * @user_data: User data for @func. + * + * Traverses node and all of its children according to the parameters + * provided. @func is called for each matching node. + * + * This assumes that the order is %G_POST_ORDER, and therefore does not + * have the conditionals to check pre-vs-post ordering. + * + * Returns: %TRUE if traversal was cancelled; otherwise %FALSE. + */ +static gboolean +dzl_trie_traverse_node_post_order (DzlTrie *trie, + DzlTrieNode *node, + GString *str, + GTraverseFlags flags, + gint max_depth, + DzlTrieTraverseFunc func, + gpointer user_data) +{ + DzlTrieNodeChunk *iter; + gboolean ret = FALSE; + guint i; + + g_assert(trie); + g_assert(node); + g_assert(str); + + if (max_depth) { + for (iter = &node->chunk; iter; iter = iter->next) { + for (i = 0; i < iter->count; i++) { + g_string_append_c(str, iter->keys[i]); + if (dzl_trie_traverse_node_post_order(trie, + iter->children[i], + str, + flags, + max_depth - 1, + func, + user_data)) { + return TRUE; + } + g_string_truncate(str, str->len - 1); + } + } + if ((!node->value && (flags & G_TRAVERSE_NON_LEAVES)) || + (node->value && (flags & G_TRAVERSE_LEAVES))) { + ret = func(trie, str->str, node->value, user_data); + } + } + + return ret; +} + +/** + * dzl_trie_traverse: + * @trie: A #DzlTrie. + * @key: The key to start traversal from. + * @order: The order to traverse. + * @flags: The flags for which nodes to callback. + * @max_depth: the maximum depth to process. + * @func: (scope call) (closure user_data): The func to execute for each matching node. + * @user_data: User data for @func. + * + * Traverses all nodes of @trie according to the parameters. For each node + * matching the traversal parameters, @func will be executed. + * + * Only %G_PRE_ORDER and %G_POST_ORDER are supported for @order. + * + * If @max_depth is less than zero, the entire tree will be traversed. + * If max_depth is 1, then only the root will be traversed. + */ +void +dzl_trie_traverse (DzlTrie *trie, + const gchar *key, + GTraverseType order, + GTraverseFlags flags, + gint max_depth, + DzlTrieTraverseFunc func, + gpointer user_data) +{ + DzlTrieNode *node; + GString *str; + + g_return_if_fail(trie); + g_return_if_fail(func); + + node = trie->root; + key = key ? key : ""; + + str = g_string_new(key); + + while (*key && node) { + node = dzl_trie_find_node(trie, node, *key); + key++; + } + + if (node) { + if (order == G_PRE_ORDER) { + dzl_trie_traverse_node_pre_order(trie, node, str, flags, max_depth, func, user_data); + } else if (order == G_POST_ORDER) { + dzl_trie_traverse_node_post_order(trie, node, str, flags, max_depth, func, user_data); + } else { + g_warning(_("Traversal order %u is not supported on DzlTrie."), order); + } + } + + g_string_free(str, TRUE); +} + +/** + * dzl_trie_unref: + * @trie: A #DzlTrie or %NULL. + * + * Drops the reference count by one on @trie. When it reaches zero, the + * structure is freed. + */ +void +dzl_trie_unref (DzlTrie *trie) +{ + g_return_if_fail(trie != NULL); + g_return_if_fail(trie->ref_count > 0); + + if (g_atomic_int_dec_and_test(&trie->ref_count)) { + dzl_trie_destroy_node(trie, trie->root, trie->value_destroy); + trie->root = NULL; + trie->value_destroy = NULL; + g_free(trie); + } +} + +DzlTrie * +dzl_trie_ref (DzlTrie *trie) +{ + g_return_val_if_fail(trie != NULL, NULL); + g_return_val_if_fail(trie->ref_count > 0, NULL); + + g_atomic_int_inc(&trie->ref_count); + return trie; +} + +/** + * dzl_trie_destroy: + * @trie: A #DzlTrie or %NULL. + * + * This is an alias for dzl_trie_unref(). + */ +void +dzl_trie_destroy (DzlTrie *trie) +{ + if (trie != NULL) + dzl_trie_unref (trie); +} diff --git a/src/search/dzl-trie.h b/src/search/dzl-trie.h new file mode 100644 index 0000000..10c4b4c --- /dev/null +++ b/src/search/dzl-trie.h @@ -0,0 +1,68 @@ +/* dzl-trie.h + * + * Copyright (C) 2012 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_TRIE_H +#define DZL_TRIE_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_TRIE (dzl_trie_get_type()) + +typedef struct _DzlTrie DzlTrie; + +typedef gboolean (*DzlTrieTraverseFunc) (DzlTrie *dzl_trie, + const gchar *key, + gpointer value, + gpointer user_data); + +DZL_AVAILABLE_IN_ALL +GType dzl_trie_get_type (void); +DZL_AVAILABLE_IN_ALL +void dzl_trie_destroy (DzlTrie *trie); +DZL_AVAILABLE_IN_ALL +void dzl_trie_unref (DzlTrie *trie); +DZL_AVAILABLE_IN_ALL +DzlTrie *dzl_trie_ref (DzlTrie *trie); +DZL_AVAILABLE_IN_ALL +void dzl_trie_insert (DzlTrie *trie, + const gchar *key, + gpointer value); +DZL_AVAILABLE_IN_ALL +gpointer dzl_trie_lookup (DzlTrie *trie, + const gchar *key); +DZL_AVAILABLE_IN_ALL +DzlTrie *dzl_trie_new (GDestroyNotify value_destroy); +DZL_AVAILABLE_IN_ALL +gboolean dzl_trie_remove (DzlTrie *trie, + const gchar *key); +DZL_AVAILABLE_IN_ALL +void dzl_trie_traverse (DzlTrie *trie, + const gchar *key, + GTraverseType order, + GTraverseFlags flags, + gint max_depth, + DzlTrieTraverseFunc func, + gpointer user_data); + +G_END_DECLS + +#endif /* DZL_TRIE_H */ diff --git a/src/search/meson.build b/src/search/meson.build new file mode 100644 index 0000000..5d8b72f --- /dev/null +++ b/src/search/meson.build @@ -0,0 +1,26 @@ +search_headers = [ + 'dzl-fuzzy-index-builder.h', + 'dzl-fuzzy-index-cursor.h', + 'dzl-fuzzy-index.h', + 'dzl-fuzzy-index-match.h', + 'dzl-fuzzy-mutable-index.h', + 'dzl-levenshtein.h', + 'dzl-pattern-spec.h', + 'dzl-trie.h', +] + +search_sources = [ + 'dzl-fuzzy-index-builder.c', + 'dzl-fuzzy-index-cursor.c', + 'dzl-fuzzy-index.c', + 'dzl-fuzzy-index-match.c', + 'dzl-fuzzy-mutable-index.c', + 'dzl-levenshtein.c', + 'dzl-pattern-spec.c', + 'dzl-trie.c', +] + +libdazzle_public_headers += files(search_headers) +libdazzle_public_sources += files(search_sources) + +install_headers(search_headers, subdir: join_paths(libdazzle_header_subdir, 'search')) diff --git a/src/settings/dzl-settings-sandwich.c b/src/settings/dzl-settings-sandwich.c new file mode 100644 index 0000000..6fd03d3 --- /dev/null +++ b/src/settings/dzl-settings-sandwich.c @@ -0,0 +1,491 @@ +/* dzl-settings-sandwich.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-settings-sandwich" +#define G_SETTINGS_ENABLE_BACKEND + +#include "config.h" + +#include +#include + +#include "dzl-settings-sandwich.h" + +struct _DzlSettingsSandwich +{ + GObject parent_instance; + GPtrArray *settings; + GSettingsBackend *memory_backend; + GSettings *memory_settings; + gchar *schema_id; + gchar *path; +}; + +G_DEFINE_TYPE (DzlSettingsSandwich, dzl_settings_sandwich, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_PATH, + PROP_SCHEMA_ID, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +static GSettings * +dzl_settings_sandwich_get_primary_settings (DzlSettingsSandwich *self) +{ + g_assert (DZL_IS_SETTINGS_SANDWICH (self)); + + if (self->settings->len == 0) + { + g_error ("No settings have been loaded. Aborting."); + g_assert_not_reached (); + return NULL; + } + + return g_ptr_array_index (self->settings, 0); +} + +static void +dzl_settings_sandwich_cache_key (DzlSettingsSandwich *self, + const gchar *key) +{ + GSettings *settings; + g_autoptr(GVariant) value = NULL; + gsize i; + + g_assert (DZL_IS_SETTINGS_SANDWICH (self)); + g_assert (key != NULL); + g_assert (self->settings->len > 0); + + for (i = 0; i < self->settings->len; i++) + { + settings = g_ptr_array_index (self->settings, i); + value = g_settings_get_user_value (settings, key); + + if (value != NULL) + { + g_settings_set_value (self->memory_settings, key, value); + return; + } + } + + settings = g_ptr_array_index (self->settings, 0); + value = g_settings_get_value (settings, key); + g_settings_set_value (self->memory_settings, key, value); +} + +static void +dzl_settings_sandwich_update_cache (DzlSettingsSandwich *self) +{ + GSettingsSchemaSource *source; + GSettingsSchema *schema; + gchar **keys; + gsize i; + + g_assert (DZL_IS_SETTINGS_SANDWICH (self)); + + source = g_settings_schema_source_get_default (); + schema = g_settings_schema_source_lookup (source, self->schema_id, TRUE); + + if (schema == NULL) + { + g_error ("Failed to locate schema: %s", self->schema_id); + return; + } + + keys = g_settings_schema_list_keys (schema); + + for (i = 0; keys [i]; i++) + dzl_settings_sandwich_cache_key (self, keys [i]); + + g_settings_schema_unref (schema); + g_strfreev (keys); +} + +static void +dzl_settings_sandwich__settings_changed (DzlSettingsSandwich *self, + const gchar *key, + GSettings *settings) +{ + g_assert (DZL_IS_SETTINGS_SANDWICH (self)); + g_assert (key != NULL); + g_assert (G_IS_SETTINGS (settings)); + + dzl_settings_sandwich_cache_key (self, key); +} + +static void +dzl_settings_sandwich_constructed (GObject *object) +{ + DzlSettingsSandwich *self = (DzlSettingsSandwich *)object; + + g_assert (DZL_IS_SETTINGS_SANDWICH (self)); + g_assert (self->schema_id != NULL); + g_assert (self->path != NULL); + + self->memory_settings = g_settings_new_with_backend_and_path (self->schema_id, + self->memory_backend, + self->path); + + G_OBJECT_CLASS (dzl_settings_sandwich_parent_class)->constructed (object); +} + +static void +dzl_settings_sandwich_finalize (GObject *object) +{ + DzlSettingsSandwich *self = (DzlSettingsSandwich *)object; + + g_clear_pointer (&self->settings, g_ptr_array_unref); + g_clear_pointer (&self->schema_id, g_free); + g_clear_pointer (&self->path, g_free); + g_clear_object (&self->memory_backend); + + G_OBJECT_CLASS (dzl_settings_sandwich_parent_class)->finalize (object); +} + +static void +dzl_settings_sandwich_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSettingsSandwich *self = DZL_SETTINGS_SANDWICH (object); + + switch (prop_id) + { + case PROP_SCHEMA_ID: + g_value_set_string (value, self->schema_id); + break; + + case PROP_PATH: + g_value_set_string (value, self->path); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_settings_sandwich_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSettingsSandwich *self = DZL_SETTINGS_SANDWICH (object); + + switch (prop_id) + { + case PROP_SCHEMA_ID: + self->schema_id = g_value_dup_string (value); + break; + + case PROP_PATH: + self->path = g_value_dup_string (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_settings_sandwich_class_init (DzlSettingsSandwichClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = dzl_settings_sandwich_constructed; + object_class->finalize = dzl_settings_sandwich_finalize; + object_class->get_property = dzl_settings_sandwich_get_property; + object_class->set_property = dzl_settings_sandwich_set_property; + + properties [PROP_SCHEMA_ID] = + g_param_spec_string ("schema-id", + "Schema Id", + "Schema Id", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_PATH] = + g_param_spec_string ("path", + "Settings Path", + "Settings Path", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_settings_sandwich_init (DzlSettingsSandwich *self) +{ + self->settings = g_ptr_array_new_with_free_func (g_object_unref); + self->memory_backend = g_memory_settings_backend_new (); +} + +DzlSettingsSandwich * +dzl_settings_sandwich_new (const gchar *schema_id, + const gchar *path) +{ + g_return_val_if_fail (schema_id != NULL, NULL); + g_return_val_if_fail (path != NULL, NULL); + + return g_object_new (DZL_TYPE_SETTINGS_SANDWICH, + "schema-id", schema_id, + "path", path, + NULL); +} + +GVariant * +dzl_settings_sandwich_get_default_value (DzlSettingsSandwich *self, + const gchar *key) +{ + GSettings *settings; + GVariant *ret; + + g_return_val_if_fail (DZL_IS_SETTINGS_SANDWICH (self), NULL); + g_return_val_if_fail (key != NULL, NULL); + + settings = dzl_settings_sandwich_get_primary_settings (self); + ret = g_settings_get_default_value (settings, key); + + return ret; +} + +GVariant * +dzl_settings_sandwich_get_user_value (DzlSettingsSandwich *self, + const gchar *key) +{ + gsize i; + + g_return_val_if_fail (DZL_IS_SETTINGS_SANDWICH (self), NULL); + g_return_val_if_fail (key != NULL, NULL); + + for (i = 0; i < self->settings->len; i++) + { + GSettings *settings; + GVariant *value; + + settings = g_ptr_array_index (self->settings, i); + value = g_settings_get_user_value (settings, key); + if (value != NULL) + return value; + } + + return NULL; +} + +GVariant * +dzl_settings_sandwich_get_value (DzlSettingsSandwich *self, + const gchar *key) +{ + GSettings *settings; + GVariant *ret; + gsize i; + + g_return_val_if_fail (DZL_IS_SETTINGS_SANDWICH (self), NULL); + g_return_val_if_fail (key != NULL, NULL); + + + for (i = 0; i < self->settings->len; i++) + { + settings = g_ptr_array_index (self->settings, i); + ret = g_settings_get_user_value (settings, key); + if (ret != NULL) + return ret; + } + + settings = dzl_settings_sandwich_get_primary_settings (self); + ret = g_settings_get_value (settings, key); + + return ret; +} + +void +dzl_settings_sandwich_set_value (DzlSettingsSandwich *self, + const gchar *key, + GVariant *value) +{ + GSettings *settings; + + g_return_if_fail (DZL_IS_SETTINGS_SANDWICH (self)); + g_return_if_fail (key != NULL); + + settings = dzl_settings_sandwich_get_primary_settings (self); + g_settings_set_value (settings, key, value); +} + +#define DEFINE_GETTER(name, ret_type, func, ...) \ +ret_type \ +dzl_settings_sandwich_get_##name (DzlSettingsSandwich *self, \ + const gchar *key) \ +{ \ + GVariant *value; \ + ret_type ret; \ + \ + g_return_val_if_fail (DZL_IS_SETTINGS_SANDWICH (self), (ret_type)0); \ + g_return_val_if_fail (key != NULL, (ret_type)0); \ + \ + value = dzl_settings_sandwich_get_value (self, key); \ + ret = g_variant_##func (value, ##__VA_ARGS__); \ + g_variant_unref (value); \ + \ + return ret; \ +} + +DEFINE_GETTER (boolean, gboolean, get_boolean) +DEFINE_GETTER (double, gdouble, get_double) +DEFINE_GETTER (int, gint, get_int32) +DEFINE_GETTER (string, gchar *, dup_string, NULL) +DEFINE_GETTER (uint, guint, get_uint32) + +#define DEFINE_SETTER(name, param_type, func) \ +void \ +dzl_settings_sandwich_set_##name (DzlSettingsSandwich *self, \ + const gchar *key, \ + param_type val) \ +{ \ + GVariant *value; \ + \ + g_return_if_fail (DZL_IS_SETTINGS_SANDWICH (self)); \ + g_return_if_fail (key != NULL); \ + \ + value = g_variant_##func (val); \ + dzl_settings_sandwich_set_value (self, key, value); \ +} + +DEFINE_SETTER (boolean, gboolean, new_boolean) +DEFINE_SETTER (double, gdouble, new_double) +DEFINE_SETTER (int, gint, new_int32) +DEFINE_SETTER (string, const gchar *, new_string) +DEFINE_SETTER (uint, guint, new_uint32) + +void +dzl_settings_sandwich_append (DzlSettingsSandwich *self, + GSettings *settings) +{ + g_return_if_fail (DZL_IS_SETTINGS_SANDWICH (self)); + g_return_if_fail (G_IS_SETTINGS (settings)); + + g_ptr_array_add (self->settings, g_object_ref (settings)); + +#if 0 + { + g_autofree gchar *schema_id = NULL; + g_autofree gchar *path = NULL; + + g_object_get (settings, + "schema-id", &schema_id, + "path", &path, + NULL); + } +#endif + + g_signal_connect_object (settings, + "changed", + G_CALLBACK (dzl_settings_sandwich__settings_changed), + self, + G_CONNECT_SWAPPED); + + dzl_settings_sandwich_update_cache (self); +} + +void +dzl_settings_sandwich_bind (DzlSettingsSandwich *self, + const gchar *key, + gpointer object, + const gchar *property, + GSettingsBindFlags flags) +{ + g_return_if_fail (DZL_IS_SETTINGS_SANDWICH (self)); + g_return_if_fail (key != NULL); + g_return_if_fail (G_IS_OBJECT (object)); + g_return_if_fail (property != NULL); + + dzl_settings_sandwich_bind_with_mapping (self, key, object, property, flags, + NULL, NULL, NULL, NULL); +} + +/** + * dzl_settings_sandwich_bind_with_mapping: + * @self: An #DzlSettingsSandwich. + * @key: the settings key to bind. + * @object (type GObject.Object): the target object. + * @property: the property on @object to apply. + * @flags: flags for the binding. + * @get_mapping: (scope notified) (closure user_data) (destroy destroy): the get mapping function + * @set_mapping: (scope notified) (closure user_data) (destroy destroy): the set mapping function + * @user_data: user data for @get_mapping and @set_mapping. + * @destroy: destroy notify for @user_data. + * + * Creates a new binding similar to g_settings_bind_with_mapping() but applying + * from the resolved value via the settings sandwich. + */ +void +dzl_settings_sandwich_bind_with_mapping (DzlSettingsSandwich *self, + const gchar *key, + gpointer object, + const gchar *property, + GSettingsBindFlags flags, + GSettingsBindGetMapping get_mapping, + GSettingsBindSetMapping set_mapping, + gpointer user_data, + GDestroyNotify destroy) +{ + GSettings *settings; + + g_return_if_fail (DZL_IS_SETTINGS_SANDWICH (self)); + g_return_if_fail (key != NULL); + g_return_if_fail (G_IS_OBJECT (object)); + g_return_if_fail (property != NULL); + + /* + * Our memory backend/settings are compiling the values from all of the layers of our + * sandwich. Therefore, we only want to map reads from the memory backend. We want to direct + * all writes to the topmost layer of the sandwich (found at index 0). + */ + if ((flags & G_SETTINGS_BIND_GET) != 0) + g_settings_bind_with_mapping (self->memory_settings, key, object, property, + (flags & ~G_SETTINGS_BIND_SET), + get_mapping, set_mapping, user_data, destroy); + + /* + * We bind writability directly to our toplevel layer of the sandwich. + */ + settings = dzl_settings_sandwich_get_primary_settings (self); + if ((flags & G_SETTINGS_BIND_SET) != 0) + g_settings_bind_with_mapping (settings, key, object, property, (flags & ~G_SETTINGS_BIND_GET), + get_mapping, set_mapping, user_data, destroy); +} + +void +dzl_settings_sandwich_unbind (DzlSettingsSandwich *self, + const gchar *property) +{ + GSettings *settings; + + g_return_if_fail (DZL_IS_SETTINGS_SANDWICH (self)); + g_return_if_fail (property != NULL); + + settings = dzl_settings_sandwich_get_primary_settings (self); + + g_settings_unbind (settings, property); + g_settings_unbind (self->memory_backend, property); +} diff --git a/src/settings/dzl-settings-sandwich.h b/src/settings/dzl-settings-sandwich.h new file mode 100644 index 0000000..ce5af5b --- /dev/null +++ b/src/settings/dzl-settings-sandwich.h @@ -0,0 +1,109 @@ +/* dzl-settings-sandwich.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_SETTINGS_SANDWICH_H +#define DZL_SETTINGS_SANDWICH_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SETTINGS_SANDWICH (dzl_settings_sandwich_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlSettingsSandwich, dzl_settings_sandwich, DZL, SETTINGS_SANDWICH, GObject) + +DZL_AVAILABLE_IN_ALL +DzlSettingsSandwich *dzl_settings_sandwich_new (const gchar *schema_id, + const gchar *path); +DZL_AVAILABLE_IN_ALL +GVariant *dzl_settings_sandwich_get_default_value (DzlSettingsSandwich *self, + const gchar *key); +DZL_AVAILABLE_IN_ALL +GVariant *dzl_settings_sandwich_get_user_value (DzlSettingsSandwich *self, + const gchar *key); +DZL_AVAILABLE_IN_ALL +GVariant *dzl_settings_sandwich_get_value (DzlSettingsSandwich *self, + const gchar *key); +DZL_AVAILABLE_IN_ALL +void dzl_settings_sandwich_set_value (DzlSettingsSandwich *self, + const gchar *key, + GVariant *value); +DZL_AVAILABLE_IN_ALL +gboolean dzl_settings_sandwich_get_boolean (DzlSettingsSandwich *self, + const gchar *key); +DZL_AVAILABLE_IN_ALL +gdouble dzl_settings_sandwich_get_double (DzlSettingsSandwich *self, + const gchar *key); +DZL_AVAILABLE_IN_ALL +gint dzl_settings_sandwich_get_int (DzlSettingsSandwich *self, + const gchar *key); +DZL_AVAILABLE_IN_ALL +gchar *dzl_settings_sandwich_get_string (DzlSettingsSandwich *self, + const gchar *key); +DZL_AVAILABLE_IN_ALL +guint dzl_settings_sandwich_get_uint (DzlSettingsSandwich *self, + const gchar *key); +DZL_AVAILABLE_IN_ALL +void dzl_settings_sandwich_set_boolean (DzlSettingsSandwich *self, + const gchar *key, + gboolean val); +DZL_AVAILABLE_IN_ALL +void dzl_settings_sandwich_set_double (DzlSettingsSandwich *self, + const gchar *key, + gdouble val); +DZL_AVAILABLE_IN_ALL +void dzl_settings_sandwich_set_int (DzlSettingsSandwich *self, + const gchar *key, + gint val); +DZL_AVAILABLE_IN_ALL +void dzl_settings_sandwich_set_string (DzlSettingsSandwich *self, + const gchar *key, + const gchar *val); +DZL_AVAILABLE_IN_ALL +void dzl_settings_sandwich_set_uint (DzlSettingsSandwich *self, + const gchar *key, + guint val); +DZL_AVAILABLE_IN_ALL +void dzl_settings_sandwich_append (DzlSettingsSandwich *self, + GSettings *settings); +DZL_AVAILABLE_IN_ALL +void dzl_settings_sandwich_bind (DzlSettingsSandwich *self, + const gchar *key, + gpointer object, + const gchar *property, + GSettingsBindFlags flags); +DZL_AVAILABLE_IN_ALL +void dzl_settings_sandwich_bind_with_mapping (DzlSettingsSandwich *self, + const gchar *key, + gpointer object, + const gchar *property, + GSettingsBindFlags flags, + GSettingsBindGetMapping get_mapping, + GSettingsBindSetMapping set_mapping, + gpointer user_data, + GDestroyNotify destroy); +DZL_AVAILABLE_IN_ALL +void dzl_settings_sandwich_unbind (DzlSettingsSandwich *self, + const gchar *property); + +G_END_DECLS + +#endif /* DZL_SETTINGS_SANDWICH_H */ diff --git a/src/settings/meson.build b/src/settings/meson.build new file mode 100644 index 0000000..5d7d398 --- /dev/null +++ b/src/settings/meson.build @@ -0,0 +1,12 @@ +settings_headers = [ + 'dzl-settings-sandwich.h', +] + +settings_sources = [ + 'dzl-settings-sandwich.c', +] + +libdazzle_public_headers += files(settings_headers) +libdazzle_public_sources += files(settings_sources) + +install_headers(settings_headers, subdir: join_paths(libdazzle_header_subdir, 'settings')) diff --git a/src/shortcuts/dzl-shortcut-accel-dialog.c b/src/shortcuts/dzl-shortcut-accel-dialog.c new file mode 100644 index 0000000..9caf72a --- /dev/null +++ b/src/shortcuts/dzl-shortcut-accel-dialog.c @@ -0,0 +1,530 @@ +/* dzl-shortcut-accel-dialog.c + * + * Copyright (C) 2016 Endless, Inc + * (C) 2017 Christian Hergert + * + * 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 2 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 . + * + * Authors: Georges Basile Stavracas Neto + * Christian Hergert + */ + +#define G_LOG_DOMAIN "dzl-shortcut-accel-dialog" + +#include "config.h" + +#include + +#include "shortcuts/dzl-shortcut-accel-dialog.h" +#include "shortcuts/dzl-shortcut-chord.h" +#include "shortcuts/dzl-shortcut-label.h" + +struct _DzlShortcutAccelDialog +{ + GtkDialog parent_instance; + + GtkStack *stack; + GtkLabel *display_label; + DzlShortcutLabel *display_shortcut; + GtkLabel *selection_label; + GtkButton *button_cancel; + GtkButton *button_set; + + GdkDevice *grab_pointer; + + gchar *shortcut_title; + DzlShortcutChord *chord; + + gulong grab_source; + + guint first_modifier; +}; + +enum { + PROP_0, + PROP_ACCELERATOR, + PROP_SHORTCUT_TITLE, + N_PROPS +}; + +G_DEFINE_TYPE (DzlShortcutAccelDialog, dzl_shortcut_accel_dialog, GTK_TYPE_DIALOG) + +static GParamSpec *properties [N_PROPS]; + +/* + * dzl_shortcut_accel_dialog_begin_grab: + * + * This function returns %G_SOURCE_REMOVE so that it may be used as + * a GSourceFunc when necessary. + * + * Returns: %G_SOURCE_REMOVE always. + */ +static gboolean +dzl_shortcut_accel_dialog_begin_grab (DzlShortcutAccelDialog *self) +{ + g_autoptr(GList) seats = NULL; + GdkWindow *window; + GdkDisplay *display; + GdkSeat *first_seat; + GdkDevice *device; + GdkDevice *pointer; + GdkGrabStatus status; + + g_assert (DZL_IS_SHORTCUT_ACCEL_DIALOG (self)); + + self->grab_source = 0; + + if (!gtk_widget_get_mapped (GTK_WIDGET (self))) + return G_SOURCE_REMOVE; + + if (NULL == (window = gtk_widget_get_window (GTK_WIDGET (self)))) + return G_SOURCE_REMOVE; + + display = gtk_widget_get_display (GTK_WIDGET (self)); + + if (NULL == (seats = gdk_display_list_seats (display))) + return G_SOURCE_REMOVE; + + first_seat = seats->data; + device = gdk_seat_get_keyboard (first_seat); + + if (device == NULL) + { + g_warning ("Keyboard grab unsuccessful, no keyboard in seat"); + return G_SOURCE_REMOVE; + } + + if (gdk_device_get_source (device) == GDK_SOURCE_KEYBOARD) + pointer = gdk_device_get_associated_device (device); + else + pointer = device; + + status = gdk_seat_grab (gdk_device_get_seat (pointer), + window, + GDK_SEAT_CAPABILITY_KEYBOARD, + FALSE, + NULL, + NULL, + NULL, + NULL); + + if (status != GDK_GRAB_SUCCESS) + return G_SOURCE_REMOVE; + + self->grab_pointer = pointer; + + g_debug ("Grab started on %s with device %s", + G_OBJECT_TYPE_NAME (self), + G_OBJECT_TYPE_NAME (device)); + + gtk_grab_add (GTK_WIDGET (self)); + + return G_SOURCE_REMOVE; +} + +static void +dzl_shortcut_accel_dialog_release_grab (DzlShortcutAccelDialog *self) +{ + g_assert (DZL_IS_SHORTCUT_ACCEL_DIALOG (self)); + + if (self->grab_pointer != NULL) + { + gdk_seat_ungrab (gdk_device_get_seat (self->grab_pointer)); + self->grab_pointer = NULL; + gtk_grab_remove (GTK_WIDGET (self)); + } +} + +static void +dzl_shortcut_accel_dialog_map (GtkWidget *widget) +{ + DzlShortcutAccelDialog *self = (DzlShortcutAccelDialog *)widget; + + g_assert (DZL_IS_SHORTCUT_ACCEL_DIALOG (self)); + + GTK_WIDGET_CLASS (dzl_shortcut_accel_dialog_parent_class)->map (widget); + + self->grab_source = + g_timeout_add_full (G_PRIORITY_LOW, + 100, + (GSourceFunc) dzl_shortcut_accel_dialog_begin_grab, + g_object_ref (self), + g_object_unref); +} + +static void +dzl_shortcut_accel_dialog_unmap (GtkWidget *widget) +{ + DzlShortcutAccelDialog *self = (DzlShortcutAccelDialog *)widget; + + g_assert (DZL_IS_SHORTCUT_ACCEL_DIALOG (self)); + + dzl_shortcut_accel_dialog_release_grab (self); + + GTK_WIDGET_CLASS (dzl_shortcut_accel_dialog_parent_class)->unmap (widget); +} + +static gboolean +dzl_shortcut_accel_dialog_is_editing (DzlShortcutAccelDialog *self) +{ + g_assert (DZL_IS_SHORTCUT_ACCEL_DIALOG (self)); + + return self->grab_pointer != NULL; +} + +static void +dzl_shortcut_accel_dialog_apply_state (DzlShortcutAccelDialog *self) +{ + g_assert (DZL_IS_SHORTCUT_ACCEL_DIALOG (self)); + + if (self->chord != NULL) + { + gtk_stack_set_visible_child_name (self->stack, "display"); + gtk_dialog_set_response_sensitive (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT, TRUE); + } + else + { + gtk_stack_set_visible_child_name (self->stack, "selection"); + gtk_dialog_set_response_sensitive (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT, FALSE); + } +} + +static gboolean +dzl_shortcut_accel_dialog_key_press_event (GtkWidget *widget, + GdkEventKey *key) +{ + DzlShortcutAccelDialog *self = (DzlShortcutAccelDialog *)widget; + + g_assert (DZL_IS_SHORTCUT_ACCEL_DIALOG (self)); + g_assert (key != NULL); + + if (dzl_shortcut_accel_dialog_is_editing (self)) + { + GdkModifierType real_mask; + guint keyval_lower; + + if (key->is_modifier) + { + /* + * If we are just starting a chord, we need to stash the modifier + * so that we know when we have finished the sequence. + */ + if (self->chord == NULL && self->first_modifier == 0) + self->first_modifier = key->keyval; + + goto chain_up; + } + + real_mask = key->state & gtk_accelerator_get_default_mod_mask (); + keyval_lower = gdk_keyval_to_lower (key->keyval); + + /* Normalize */ + if (keyval_lower == GDK_KEY_ISO_Left_Tab) + keyval_lower = GDK_KEY_Tab; + + /* Put shift back if it changed the case of the key */ + if (keyval_lower != key->keyval) + real_mask |= GDK_SHIFT_MASK; + + /* We don't want to use SysRq as a keybinding but we do + * want Alt+Print), so we avoid translation from Alt+Print to SysRq + */ + if (keyval_lower == GDK_KEY_Sys_Req && (real_mask & GDK_MOD1_MASK) != 0) + keyval_lower = GDK_KEY_Print; + + /* A single Escape press cancels the editing */ + if (!key->is_modifier && real_mask == 0 && keyval_lower == GDK_KEY_Escape) + { + dzl_shortcut_accel_dialog_release_grab (self); + gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_CANCEL); + return GDK_EVENT_STOP; + } + + /* Backspace disables the current shortcut */ + if (real_mask == 0 && keyval_lower == GDK_KEY_BackSpace) + { + dzl_shortcut_accel_dialog_set_accelerator (self, NULL); + gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT); + return GDK_EVENT_STOP; + } + + if (self->chord == NULL) + self->chord = dzl_shortcut_chord_new_from_event (key); + else + dzl_shortcut_chord_append_event (self->chord, key); + + dzl_shortcut_accel_dialog_apply_state (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACCELERATOR]); + + return GDK_EVENT_STOP; + } + +chain_up: + return GTK_WIDGET_CLASS (dzl_shortcut_accel_dialog_parent_class)->key_press_event (widget, key); +} + +static gboolean +dzl_shortcut_accel_dialog_key_release_event (GtkWidget *widget, + GdkEventKey *key) +{ + DzlShortcutAccelDialog *self = (DzlShortcutAccelDialog *)widget; + + g_assert (DZL_IS_SHORTCUT_ACCEL_DIALOG (self)); + g_assert (key != NULL); + + if (self->chord != NULL) + { + /* + * If we have a chord defined and there was no modifier, + * then any key release should be enough for us to cancel + * our grab. + */ + if (!dzl_shortcut_chord_has_modifier (self->chord)) + { + dzl_shortcut_accel_dialog_release_grab (self); + goto chain_up; + } + + /* + * If we started our sequence with a modifier, we want to + * release our grab when that modifier has been released. + */ + if (key->is_modifier && + self->first_modifier != 0 && + self->first_modifier == key->keyval) + { + self->first_modifier = 0; + dzl_shortcut_accel_dialog_release_grab (self); + goto chain_up; + } + } + + /* Clear modifier if it was released before a chord was made */ + if (self->first_modifier == key->keyval) + self->first_modifier = 0; + +chain_up: + return GTK_WIDGET_CLASS (dzl_shortcut_accel_dialog_parent_class)->key_release_event (widget, key); +} + +static void +dzl_shortcut_accel_dialog_destroy (GtkWidget *widget) +{ + DzlShortcutAccelDialog *self = (DzlShortcutAccelDialog *)widget; + + g_assert (DZL_IS_SHORTCUT_ACCEL_DIALOG (self)); + + if (self->grab_source != 0) + { + g_source_remove (self->grab_source); + self->grab_source = 0; + } + + GTK_WIDGET_CLASS (dzl_shortcut_accel_dialog_parent_class)->destroy (widget); +} + +static void +dzl_shortcut_accel_dialog_finalize (GObject *object) +{ + DzlShortcutAccelDialog *self = (DzlShortcutAccelDialog *)object; + + g_clear_pointer (&self->shortcut_title, g_free); + g_clear_pointer (&self->chord, dzl_shortcut_chord_free); + + G_OBJECT_CLASS (dzl_shortcut_accel_dialog_parent_class)->finalize (object); +} + +static void +dzl_shortcut_accel_dialog_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutAccelDialog *self = DZL_SHORTCUT_ACCEL_DIALOG (object); + + switch (prop_id) + { + case PROP_ACCELERATOR: + g_value_take_string (value, dzl_shortcut_accel_dialog_get_accelerator (self)); + break; + + case PROP_SHORTCUT_TITLE: + g_value_set_string (value, dzl_shortcut_accel_dialog_get_shortcut_title (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_accel_dialog_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutAccelDialog *self = DZL_SHORTCUT_ACCEL_DIALOG (object); + + switch (prop_id) + { + case PROP_ACCELERATOR: + dzl_shortcut_accel_dialog_set_accelerator (self, g_value_get_string (value)); + break; + + case PROP_SHORTCUT_TITLE: + dzl_shortcut_accel_dialog_set_shortcut_title (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_accel_dialog_class_init (DzlShortcutAccelDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_shortcut_accel_dialog_finalize; + object_class->get_property = dzl_shortcut_accel_dialog_get_property; + object_class->set_property = dzl_shortcut_accel_dialog_set_property; + + widget_class->destroy = dzl_shortcut_accel_dialog_destroy; + widget_class->map = dzl_shortcut_accel_dialog_map; + widget_class->unmap = dzl_shortcut_accel_dialog_unmap; + widget_class->key_press_event = dzl_shortcut_accel_dialog_key_press_event; + widget_class->key_release_event = dzl_shortcut_accel_dialog_key_release_event; + + properties [PROP_ACCELERATOR] = + g_param_spec_string ("accelerator", + "Accelerator", + "Accelerator", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHORTCUT_TITLE] = + g_param_spec_string ("shortcut-title", + "Title", + "Title", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-shortcut-accel-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, DzlShortcutAccelDialog, stack); + gtk_widget_class_bind_template_child (widget_class, DzlShortcutAccelDialog, selection_label); + gtk_widget_class_bind_template_child (widget_class, DzlShortcutAccelDialog, display_label); + gtk_widget_class_bind_template_child (widget_class, DzlShortcutAccelDialog, display_shortcut); + gtk_widget_class_bind_template_child (widget_class, DzlShortcutAccelDialog, button_cancel); + gtk_widget_class_bind_template_child (widget_class, DzlShortcutAccelDialog, button_set); + + g_type_ensure (DZL_TYPE_SHORTCUT_LABEL); +} + +static void +dzl_shortcut_accel_dialog_init (DzlShortcutAccelDialog *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_dialog_add_buttons (GTK_DIALOG (self), + _("Cancel"), GTK_RESPONSE_CANCEL, + _("Set"), GTK_RESPONSE_ACCEPT, + NULL); + gtk_dialog_set_default_response (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT); + + gtk_dialog_set_response_sensitive (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT, FALSE); + + g_object_bind_property (self, "accelerator", + self->display_shortcut, "accelerator", + G_BINDING_SYNC_CREATE); +} + +gchar * +dzl_shortcut_accel_dialog_get_accelerator (DzlShortcutAccelDialog *self) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_ACCEL_DIALOG (self), NULL); + + if (self->chord == NULL) + return NULL; + + return dzl_shortcut_chord_to_string (self->chord); +} + +void +dzl_shortcut_accel_dialog_set_accelerator (DzlShortcutAccelDialog *self, + const gchar *accelerator) +{ + g_autoptr(DzlShortcutChord) chord = NULL; + + g_return_if_fail (DZL_IS_SHORTCUT_ACCEL_DIALOG (self)); + + if (accelerator) + chord = dzl_shortcut_chord_new_from_string (accelerator); + + if (!dzl_shortcut_chord_equal (chord, self->chord)) + { + dzl_shortcut_chord_free (self->chord); + self->chord = g_steal_pointer (&chord); + gtk_dialog_set_response_sensitive (GTK_DIALOG (self), + GTK_RESPONSE_ACCEPT, + self->chord != NULL); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACCELERATOR]); + } +} + +void +dzl_shortcut_accel_dialog_set_shortcut_title (DzlShortcutAccelDialog *self, + const gchar *shortcut_title) +{ + g_return_if_fail (DZL_IS_SHORTCUT_ACCEL_DIALOG (self)); + + if (g_strcmp0 (shortcut_title, self->shortcut_title) != 0) + { + g_autofree gchar *label = NULL; + + if (shortcut_title != NULL) + { + /* Translators: %s is used to show the provided text in bold */ + label = g_strdup_printf (_("Enter new shortcut to change %s."), shortcut_title); + } + + gtk_label_set_label (self->selection_label, label); + gtk_label_set_label (self->display_label, label); + + g_free (self->shortcut_title); + self->shortcut_title = g_strdup (shortcut_title); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHORTCUT_TITLE]); + } +} + +const gchar * +dzl_shortcut_accel_dialog_get_shortcut_title (DzlShortcutAccelDialog *self) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_ACCEL_DIALOG (self), NULL); + + return self->shortcut_title; +} + +const DzlShortcutChord * +dzl_shortcut_accel_dialog_get_chord (DzlShortcutAccelDialog *self) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_ACCEL_DIALOG (self), NULL); + + return self->chord; +} diff --git a/src/shortcuts/dzl-shortcut-accel-dialog.h b/src/shortcuts/dzl-shortcut-accel-dialog.h new file mode 100644 index 0000000..770e44c --- /dev/null +++ b/src/shortcuts/dzl-shortcut-accel-dialog.h @@ -0,0 +1,52 @@ +/* dzl-shortcut-accel-dialog.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#ifndef DZL_SHORTCUT_ACCEL_DIALOG_H +#define DZL_SHORTCUT_ACCEL_DIALOG_H + +#include + +#include "dzl-version-macros.h" + +#include "dzl-shortcut-chord.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUT_ACCEL_DIALOG (dzl_shortcut_accel_dialog_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlShortcutAccelDialog, dzl_shortcut_accel_dialog, DZL, SHORTCUT_ACCEL_DIALOG, GtkDialog) + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_shortcut_accel_dialog_new (void); +DZL_AVAILABLE_IN_ALL +gchar *dzl_shortcut_accel_dialog_get_accelerator (DzlShortcutAccelDialog *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_accel_dialog_set_accelerator (DzlShortcutAccelDialog *self, + const gchar *accelerator); +DZL_AVAILABLE_IN_ALL +const DzlShortcutChord *dzl_shortcut_accel_dialog_get_chord (DzlShortcutAccelDialog *self); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_shortcut_accel_dialog_get_shortcut_title (DzlShortcutAccelDialog *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_accel_dialog_set_shortcut_title (DzlShortcutAccelDialog *self, + const gchar *title); + +G_END_DECLS + +#endif /* DZL_SHORTCUT_ACCEL_DIALOG_H */ diff --git a/src/shortcuts/dzl-shortcut-accel-dialog.ui b/src/shortcuts/dzl-shortcut-accel-dialog.ui new file mode 100644 index 0000000..f58ef28 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-accel-dialog.ui @@ -0,0 +1,90 @@ + + + + diff --git a/src/shortcuts/dzl-shortcut-chord.c b/src/shortcuts/dzl-shortcut-chord.c new file mode 100644 index 0000000..0706173 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-chord.c @@ -0,0 +1,736 @@ +/* dzl-shortcut-chord.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-chord" + +#include "config.h" + +#include +#include + +#include "shortcuts/dzl-shortcut-chord.h" +#include "shortcuts/dzl-shortcut-private.h" + +#define MAX_CHORD_SIZE 4 + +G_DEFINE_BOXED_TYPE (DzlShortcutChord, dzl_shortcut_chord, + dzl_shortcut_chord_copy, dzl_shortcut_chord_free) +G_DEFINE_POINTER_TYPE (DzlShortcutChordTable, dzl_shortcut_chord_table) + +typedef struct +{ + guint keyval; + GdkModifierType modifier; +} DzlShortcutKey; + +struct _DzlShortcutChord +{ + DzlShortcutKey keys[MAX_CHORD_SIZE]; +}; + +typedef struct +{ + DzlShortcutChord chord; + gpointer data; +} DzlShortcutChordTableEntry; + +struct _DzlShortcutChordTable +{ + DzlShortcutChordTableEntry *entries; + GDestroyNotify destroy; + guint len; + guint size; +}; + +static GdkModifierType +sanitize_modifier_mask (GdkModifierType mods) +{ + mods &= gtk_accelerator_get_default_mod_mask (); + mods &= ~GDK_LOCK_MASK; + + return mods; +} + +static gint +dzl_shortcut_chord_compare (const DzlShortcutChord *a, + const DzlShortcutChord *b) +{ + return memcmp (a, b, sizeof *a); +} + +static gboolean +dzl_shortcut_chord_is_valid (DzlShortcutChord *self) +{ + g_assert (self != NULL); + + /* Ensure we got a valid first key at least */ + if (self->keys[0].keyval == 0) + return FALSE; + + return TRUE; +} + +DzlShortcutChord * +dzl_shortcut_chord_new_from_event (const GdkEventKey *key) +{ + DzlShortcutChord *self; + + g_return_val_if_fail (key != NULL, NULL); + + /* Ignore modifier keypresses */ + if (key->is_modifier) + return NULL; + + self = g_slice_new0 (DzlShortcutChord); + + self->keys[0].keyval = gdk_keyval_to_lower (key->keyval); + self->keys[0].modifier = sanitize_modifier_mask (key->state); + + if ((key->state & GDK_LOCK_MASK) == 0 && + self->keys[0].keyval != key->keyval) + self->keys[0].modifier |= GDK_SHIFT_MASK; + + if (!dzl_shortcut_chord_is_valid (self)) + g_clear_pointer (&self, dzl_shortcut_chord_free); + + return self; +} + +DzlShortcutChord * +dzl_shortcut_chord_new_from_string (const gchar *accelerator) +{ + DzlShortcutChord *self; + g_auto(GStrv) parts = NULL; + + g_return_val_if_fail (accelerator != NULL, NULL); + + /* We might have a single key, or chord defined */ + parts = g_strsplit (accelerator, "|", 0); + + /* Make sure we won't overflow the keys array */ + if (g_strv_length (parts) > G_N_ELEMENTS (self->keys)) + return NULL; + + self = g_slice_new0 (DzlShortcutChord); + + /* Parse each section from the accelerator */ + for (guint i = 0; parts[i]; i++) + gtk_accelerator_parse (parts[i], &self->keys[i].keyval, &self->keys[i].modifier); + + /* Ensure we got a valid first key at least */ + if (!dzl_shortcut_chord_is_valid (self)) + g_clear_pointer (&self, dzl_shortcut_chord_free); + + return self; +} + +gboolean +dzl_shortcut_chord_append_event (DzlShortcutChord *self, + const GdkEventKey *key) +{ + guint i; + + g_return_val_if_fail (self != NULL, FALSE); + g_return_val_if_fail (key != NULL, FALSE); + + for (i = 0; i < G_N_ELEMENTS (self->keys); i++) + { + if (self->keys[i].keyval == 0) + { + self->keys[i].keyval = gdk_keyval_to_lower (key->keyval); + self->keys[i].modifier = sanitize_modifier_mask (key->state); + + if ((key->state & GDK_LOCK_MASK) == 0 && + self->keys[i].keyval != key->keyval) + self->keys[i].modifier |= GDK_SHIFT_MASK; + + return TRUE; + } + } + + return FALSE; +} + +static inline guint +dzl_shortcut_chord_count_keys (const DzlShortcutChord *self) +{ + guint count = 0; + + for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++) + { + if (self->keys[i].keyval != 0) + count++; + else + break; + } + + return count; +} + +DzlShortcutMatch +dzl_shortcut_chord_match (const DzlShortcutChord *self, + const DzlShortcutChord *other) +{ + guint self_count = 0; + guint other_count = 0; + + g_return_val_if_fail (self != NULL, DZL_SHORTCUT_MATCH_NONE); + g_return_val_if_fail (other != NULL, DZL_SHORTCUT_MATCH_NONE); + + self_count = dzl_shortcut_chord_count_keys (self); + other_count = dzl_shortcut_chord_count_keys (other); + + if (self_count > other_count) + return DZL_SHORTCUT_MATCH_NONE; + + if (0 == memcmp (self->keys, other->keys, sizeof (DzlShortcutKey) * self_count)) + return self_count == other_count ? DZL_SHORTCUT_MATCH_EQUAL : DZL_SHORTCUT_MATCH_PARTIAL; + + return DZL_SHORTCUT_MATCH_NONE; +} + +gchar * +dzl_shortcut_chord_to_string (const DzlShortcutChord *self) +{ + GString *str; + + if (self == NULL || self->keys[0].keyval == 0) + return NULL; + + str = g_string_new (NULL); + + for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++) + { + const DzlShortcutKey *key = &self->keys[i]; + g_autofree gchar *name = NULL; + + if (key->keyval == 0 && key->modifier == 0) + break; + + name = gtk_accelerator_name (key->keyval, key->modifier); + + if (i != 0) + g_string_append_c (str, '|'); + + g_string_append (str, name); + } + + return g_string_free (str, FALSE); +} + +gchar * +dzl_shortcut_chord_get_label (const DzlShortcutChord *self) +{ + GString *str; + + if (self == NULL || self->keys[0].keyval == 0) + return NULL; + + str = g_string_new (NULL); + + for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++) + { + const DzlShortcutKey *key = &self->keys[i]; + g_autofree gchar *name = NULL; + + if (key->keyval == 0 && key->modifier == 0) + break; + + name = gtk_accelerator_get_label (key->keyval, key->modifier); + + if (i != 0) + g_string_append_c (str, ' '); + + g_string_append (str, name); + } + + return g_string_free (str, FALSE); +} + +DzlShortcutChord * +dzl_shortcut_chord_copy (const DzlShortcutChord *self) +{ + DzlShortcutChord *copy; + + if (self == NULL) + return NULL; + + copy = g_slice_new (DzlShortcutChord); + memcpy (copy, self, sizeof *copy); + + return copy; +} + +guint +dzl_shortcut_chord_hash (gconstpointer data) +{ + const DzlShortcutChord *self = data; + guint hash = 0; + + for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++) + { + const DzlShortcutKey *key = &self->keys[i]; + + hash ^= key->keyval; + hash ^= key->modifier; + } + + return hash; +} + +gboolean +dzl_shortcut_chord_equal (gconstpointer data1, + gconstpointer data2) +{ + if (data1 == data2) + return TRUE; + else if (data1 == NULL || data2 == NULL) + return FALSE; + + return 0 == memcmp (((const DzlShortcutChord *)data1)->keys, + ((const DzlShortcutChord *)data2)->keys, + sizeof (DzlShortcutChord)); +} + +void +dzl_shortcut_chord_free (DzlShortcutChord *self) +{ + if (self != NULL) + g_slice_free (DzlShortcutChord, self); +} + +GType +dzl_shortcut_match_get_type (void) +{ + static GType type_id; + + if (g_once_init_enter (&type_id)) + { + static GEnumValue values[] = { + { DZL_SHORTCUT_MATCH_NONE, "DZL_SHORTCUT_MATCH_NONE", "none" }, + { DZL_SHORTCUT_MATCH_EQUAL, "DZL_SHORTCUT_MATCH_EQUAL", "equal" }, + { DZL_SHORTCUT_MATCH_PARTIAL, "DZL_SHORTCUT_MATCH_PARTIAL", "partial" }, + { 0 } + }; + GType _type_id = g_enum_register_static ("DzlShortcutMatch", values); + g_once_init_leave (&type_id, _type_id); + } + + return type_id; +} + +static gint +dzl_shortcut_chord_table_sort (gconstpointer a, + gconstpointer b) +{ + const DzlShortcutChordTableEntry *keya = a; + const DzlShortcutChordTableEntry *keyb = b; + + return dzl_shortcut_chord_compare (&keya->chord, &keyb->chord); +} + +/** + * dzl_shortcut_chord_table_new: (skip) + */ +DzlShortcutChordTable * +dzl_shortcut_chord_table_new (void) +{ + DzlShortcutChordTable *table; + + table = g_slice_new0 (DzlShortcutChordTable); + table->len = 0; + table->size = 4; + table->destroy = NULL; + table->entries = g_new0 (DzlShortcutChordTableEntry, table->size); + + return table; +} + +void +dzl_shortcut_chord_table_free (DzlShortcutChordTable *self) +{ + if (self != NULL) + { + if (self->destroy != NULL) + { + for (guint i = 0; i < self->len; i++) + self->destroy (self->entries[i].data); + } + g_free (self->entries); + g_slice_free (DzlShortcutChordTable, self); + } +} + +void +dzl_shortcut_chord_table_add (DzlShortcutChordTable *self, + const DzlShortcutChord *chord, + gpointer data) +{ + g_return_if_fail (self != NULL); + g_return_if_fail (chord != NULL); + + if (self->len == self->size) + { + self->size *= 2; + self->entries = g_renew (DzlShortcutChordTableEntry, self->entries, self->size); + } + + self->entries[self->len].chord = *chord; + self->entries[self->len].data = data; + + self->len++; + + qsort (self->entries, + self->len, + sizeof (DzlShortcutChordTableEntry), + dzl_shortcut_chord_table_sort); +} + +static void +dzl_shortcut_chord_table_remove_index (DzlShortcutChordTable *self, + guint position) +{ + const DzlShortcutChordTableEntry *entry; + gpointer data; + + g_assert (self != NULL); + g_assert (position < self->len); + + entry = &self->entries[position]; + data = entry->data; + + if (position + 1 < self->len) + memmove ((gpointer)entry, + entry + 1, + sizeof *entry * (self->len - position - 1)); + + self->len--; + + if (self->destroy != NULL) + self->destroy (data); +} + +gboolean +dzl_shortcut_chord_table_remove (DzlShortcutChordTable *self, + const DzlShortcutChord *chord) +{ + g_return_val_if_fail (self != NULL, FALSE); + + if (chord == NULL) + return FALSE; + + for (guint i = 0; i < self->len; i++) + { + const DzlShortcutChordTableEntry *ele = &self->entries[i]; + + if (dzl_shortcut_chord_equal (&ele->chord, chord)) + { + dzl_shortcut_chord_table_remove_index (self, i); + return TRUE; + } + } + + return FALSE; +} + +gboolean +dzl_shortcut_chord_table_remove_data (DzlShortcutChordTable *self, + gpointer data) +{ + g_return_val_if_fail (self != NULL, FALSE); + + for (guint i = 0; i < self->len; i++) + { + const DzlShortcutChordTableEntry *ele = &self->entries[i]; + + if (ele->data == data) + { + dzl_shortcut_chord_table_remove_index (self, i); + return TRUE; + } + } + + return FALSE; +} + +const DzlShortcutChord * +dzl_shortcut_chord_table_lookup_data (DzlShortcutChordTable *self, + gpointer data) +{ + if (self == NULL) + return NULL; + + for (guint i = 0; i < self->len; i++) + { + const DzlShortcutChordTableEntry *ele = &self->entries[i]; + + if (ele->data == data) + return &ele->chord; + } + + return NULL; +} + +static gint +dzl_shortcut_chord_find_partial (gconstpointer a, + gconstpointer b) +{ + const DzlShortcutChord *key = a; + const DzlShortcutChordTableEntry *element = b; + + /* + * We are only looking for a partial match here so that we can walk backwards + * after the bsearch to the first partial match. + */ + if (dzl_shortcut_chord_match (key, &element->chord) != DZL_SHORTCUT_MATCH_NONE) + return 0; + + return dzl_shortcut_chord_compare (key, &element->chord); +} + +DzlShortcutMatch +dzl_shortcut_chord_table_lookup (DzlShortcutChordTable *self, + const DzlShortcutChord *chord, + gpointer *data) +{ + const DzlShortcutChordTableEntry *match; + + if (data != NULL) + *data = NULL; + + if (self == NULL) + return DZL_SHORTCUT_MATCH_NONE; + + if (chord == NULL) + return DZL_SHORTCUT_MATCH_NONE; + + if (self->len == 0) + return DZL_SHORTCUT_MATCH_NONE; + + /* + * This function works by performing a binary search to locate ourself + * somewhere within a match zone of the array. Once we are there, we walk + * back to the first item that is a partial match. After that, we walk + * through every potential match looking for an exact match until we reach a + * non-partial-match or the end of the array. + * + * Based on our findings, we return the appropriate DzlShortcutMatch. + */ + + match = bsearch (chord, self->entries, self->len, sizeof (DzlShortcutChordTableEntry), + dzl_shortcut_chord_find_partial); + + if (match != NULL) + { + const DzlShortcutChordTableEntry *begin = self->entries; + const DzlShortcutChordTableEntry *end = self->entries + self->len; + DzlShortcutMatch ret = DZL_SHORTCUT_MATCH_PARTIAL; + + /* Find the first patial match */ + while ((match - 1) >= begin && + dzl_shortcut_chord_match (chord, &(match - 1)->chord) != DZL_SHORTCUT_MATCH_NONE) + match--; + + g_assert (match >= begin); + + /* Now walk forward to see if we have an exact match */ + while (DZL_SHORTCUT_MATCH_NONE != (ret = dzl_shortcut_chord_match (chord, &match->chord))) + { + if (ret == DZL_SHORTCUT_MATCH_EQUAL) + { + if (data != NULL) + *data = match->data; + return DZL_SHORTCUT_MATCH_EQUAL; + } + + match++; + + g_assert (match <= end); + + if (ret == 0 || match == end) + break; + } + + return DZL_SHORTCUT_MATCH_PARTIAL; + } + + return DZL_SHORTCUT_MATCH_NONE; +} + +void +dzl_shortcut_chord_table_set_free_func (DzlShortcutChordTable *self, + GDestroyNotify destroy) +{ + g_return_if_fail (self != NULL); + + self->destroy = destroy; +} + +guint +dzl_shortcut_chord_table_size (const DzlShortcutChordTable *self) +{ + /* I know this is confusing, but len is the number of items in the + * table (which consumers think of as size), and @size is the allocated + * size of the ^2 growing array. + */ + return self ? self->len : 0; +} + +void +dzl_shortcut_chord_table_printf (const DzlShortcutChordTable *self) +{ + if (self == NULL) + return; + + for (guint i = 0; i < self->len; i++) + { + const DzlShortcutChordTableEntry *entry = &self->entries[i]; + g_autofree gchar *str = dzl_shortcut_chord_to_string (&entry->chord); + + g_print ("%s\n", str); + } +} + +void +_dzl_shortcut_chord_table_iter_init (DzlShortcutChordTableIter *iter, + DzlShortcutChordTable *table) +{ + g_return_if_fail (iter != NULL); + + iter->table = table; + iter->position = 0; +} + +gboolean +_dzl_shortcut_chord_table_iter_next (DzlShortcutChordTableIter *iter, + const DzlShortcutChord **chord, + gpointer *value) +{ + g_return_val_if_fail (iter != NULL, FALSE); + + /* + * Be safe against NULL tables which we allow in + * _dzl_shortcut_chord_table_iter_init() for convenience. + */ + if (iter->table == NULL) + return FALSE; + + if (iter->position < iter->table->len) + { + *chord = &iter->table->entries[iter->position].chord; + *value = iter->table->entries[iter->position].data; + iter->position++; + return TRUE; + } + + return FALSE; +} + +void +_dzl_shortcut_chord_table_iter_steal (DzlShortcutChordTableIter *iter) +{ + g_return_if_fail (iter != NULL); + g_return_if_fail (iter->table != NULL); + + if (iter->position > 0 && iter->position < iter->table->len) + { + dzl_shortcut_chord_table_remove_index (iter->table, --iter->position); + return; + } + + g_warning ("Attempt to steal item from table that does not exist"); +} + +gboolean +dzl_shortcut_chord_has_modifier (const DzlShortcutChord *self) +{ + g_return_val_if_fail (self != NULL, FALSE); + + return self->keys[0].modifier != 0; +} + +guint +dzl_shortcut_chord_get_length (const DzlShortcutChord *self) +{ + if (self != NULL) + { + for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++) + { + if (self->keys[i].keyval == 0) + return i; + } + + return G_N_ELEMENTS (self->keys); + } + + return 0; +} + +void +dzl_shortcut_chord_get_nth_key (const DzlShortcutChord *self, + guint nth, + guint *keyval, + GdkModifierType *modifier) +{ + if (nth < G_N_ELEMENTS (self->keys)) + { + if (keyval) + *keyval = self->keys[nth].keyval; + if (modifier) + *modifier = self->keys[nth].modifier; + } + else + { + if (keyval) + *keyval = 0; + if (modifier) + *modifier = 0; + } +} + +/** + * dzl_shortcut_chord_table_foreach: + * @self: a #DzlShortcutChordTable + * @foreach_func: (scope call) (closure foreach_data): A callback for each chord + * @foreach_data: user data for @foreach_func + * + * This function will call @foreach_func for each chord in the table. + */ +void +dzl_shortcut_chord_table_foreach (const DzlShortcutChordTable *self, + DzlShortcutChordTableForeach foreach_func, + gpointer foreach_data) +{ + g_return_if_fail (foreach_func != NULL); + + if (self == NULL) + return; + + /* + * Walk backwards just in case the caller somehow thinks it is okay to + * remove items while iterating the list. We don't officially support that + * (which is why self is const), but this is just defensive. + */ + + for (guint i = self->len; i > 0; i--) + { + const DzlShortcutChordTableEntry *entry = &self->entries[i-1]; + + foreach_func (&entry->chord, entry->data, foreach_data); + } +} diff --git a/src/shortcuts/dzl-shortcut-chord.h b/src/shortcuts/dzl-shortcut-chord.h new file mode 100644 index 0000000..d2dd5c3 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-chord.h @@ -0,0 +1,121 @@ +/* dzl-shortcut-chord.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#ifndef DZL_SHORTCUT_CHORD_H +#define DZL_SHORTCUT_CHORD_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +typedef enum +{ + DZL_SHORTCUT_MATCH_NONE, + DZL_SHORTCUT_MATCH_EQUAL, + DZL_SHORTCUT_MATCH_PARTIAL +} DzlShortcutMatch; + +#define DZL_TYPE_SHORTCUT_CHORD (dzl_shortcut_chord_get_type()) +#define DZL_TYPE_SHORTCUT_CHORD_TABLE (dzl_shortcut_chord_table_get_type()) +#define DZL_TYPE_SHORTCUT_MATCH (dzl_shortcut_match_get_type()) + +typedef struct _DzlShortcutChord DzlShortcutChord; +typedef struct _DzlShortcutChordTable DzlShortcutChordTable; + +typedef void (*DzlShortcutChordTableForeach) (const DzlShortcutChord *chord, + gpointer chord_data, + gpointer user_data); + +DZL_AVAILABLE_IN_ALL +GType dzl_shortcut_chord_get_type (void); +DZL_AVAILABLE_IN_ALL +DzlShortcutChord *dzl_shortcut_chord_new_from_event (const GdkEventKey *event); +DZL_AVAILABLE_IN_ALL +DzlShortcutChord *dzl_shortcut_chord_new_from_string (const gchar *accelerator); +DZL_AVAILABLE_IN_ALL +gchar *dzl_shortcut_chord_to_string (const DzlShortcutChord *self); +DZL_AVAILABLE_IN_ALL +gchar *dzl_shortcut_chord_get_label (const DzlShortcutChord *self); +DZL_AVAILABLE_IN_ALL +guint dzl_shortcut_chord_get_length (const DzlShortcutChord *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_chord_get_nth_key (const DzlShortcutChord *self, + guint nth, + guint *keyval, + GdkModifierType *modifier); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_chord_has_modifier (const DzlShortcutChord *self); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_chord_append_event (DzlShortcutChord *self, + const GdkEventKey *event); +DZL_AVAILABLE_IN_ALL +DzlShortcutMatch dzl_shortcut_chord_match (const DzlShortcutChord *self, + const DzlShortcutChord *other); +DZL_AVAILABLE_IN_ALL +guint dzl_shortcut_chord_hash (gconstpointer data); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_chord_equal (gconstpointer data1, + gconstpointer data2); +DZL_AVAILABLE_IN_ALL +DzlShortcutChord *dzl_shortcut_chord_copy (const DzlShortcutChord *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_chord_free (DzlShortcutChord *self); +DZL_AVAILABLE_IN_ALL +GType dzl_shortcut_chord_table_get_type (void); +DZL_AVAILABLE_IN_ALL +DzlShortcutChordTable *dzl_shortcut_chord_table_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_chord_table_set_free_func (DzlShortcutChordTable *self, + GDestroyNotify notify); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_chord_table_free (DzlShortcutChordTable *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_chord_table_add (DzlShortcutChordTable *self, + const DzlShortcutChord *chord, + gpointer data); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_chord_table_remove (DzlShortcutChordTable *self, + const DzlShortcutChord *chord); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_chord_table_remove_data (DzlShortcutChordTable *self, + gpointer data); +DZL_AVAILABLE_IN_ALL +DzlShortcutMatch dzl_shortcut_chord_table_lookup (DzlShortcutChordTable *self, + const DzlShortcutChord *chord, + gpointer *data); +DZL_AVAILABLE_IN_ALL +const DzlShortcutChord *dzl_shortcut_chord_table_lookup_data (DzlShortcutChordTable *self, + gpointer data); +DZL_AVAILABLE_IN_ALL +guint dzl_shortcut_chord_table_size (const DzlShortcutChordTable *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_chord_table_foreach (const DzlShortcutChordTable *self, + DzlShortcutChordTableForeach foreach_func, + gpointer foreach_data); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_chord_table_printf (const DzlShortcutChordTable *self); +DZL_AVAILABLE_IN_ALL +GType dzl_shortcut_match_get_type (void); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (DzlShortcutChord, dzl_shortcut_chord_free) + +G_END_DECLS + +#endif /* DZL_SHORTCUT_CHORD_H */ diff --git a/src/shortcuts/dzl-shortcut-closure-chain.c b/src/shortcuts/dzl-shortcut-closure-chain.c new file mode 100644 index 0000000..b53cda2 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-closure-chain.c @@ -0,0 +1,464 @@ +/* dzl-shortcut-closure-chain.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-closure-chain" + +#include "config.h" + +#include +#include + +#include "dzl-debug.h" + +#include "shortcuts/dzl-shortcut-closure-chain.h" +#include "shortcuts/dzl-shortcut-controller.h" +#include "shortcuts/dzl-shortcut-private.h" +#include "util/dzl-gtk.h" +#include "util/dzl-util-private.h" + +static DzlShortcutClosureChain * +dzl_shortcut_closure_chain_new (DzlShortcutClosureType type) +{ + DzlShortcutClosureChain *ret; + + g_assert (type > 0); + g_assert (type < DZL_SHORTCUT_CLOSURE_LAST); + + ret = g_slice_new0 (DzlShortcutClosureChain); + ret->node.data = ret; + ret->type = type; + + return ret; +} + +DzlShortcutClosureChain * +dzl_shortcut_closure_chain_append (DzlShortcutClosureChain *chain, + DzlShortcutClosureChain *element) +{ + DzlShortcutClosureChain *ret; + + g_return_val_if_fail (chain || element, NULL); + + if (chain == NULL) + return element; + + if (element == NULL) + return chain; + + ret = g_slist_concat (&chain->node, &element->node)->data; + + g_return_val_if_fail (ret != NULL, NULL); + + return ret; +} + +void +dzl_shortcut_closure_chain_free (DzlShortcutClosureChain *chain) +{ + if (chain == NULL) + return; + + if (chain->executing) + { + g_warning ("Attempt to dispose a closure chain while executing, leaking"); + return; + } + + if (chain->node.next) + dzl_shortcut_closure_chain_free (chain->node.next->data); + + if (chain->type == DZL_SHORTCUT_CLOSURE_ACTION) + g_clear_pointer (&chain->action.params, g_variant_unref); + else if (chain->type == DZL_SHORTCUT_CLOSURE_CALLBACK) + { + if (chain->callback.notify) + g_clear_pointer (&chain->callback.user_data, chain->callback.notify); + } + else if (chain->type == DZL_SHORTCUT_CLOSURE_SIGNAL) + g_clear_pointer (&chain->signal.params, g_array_unref); + + g_slice_free (DzlShortcutClosureChain, chain); +} + +DzlShortcutClosureChain * +dzl_shortcut_closure_chain_append_callback (DzlShortcutClosureChain *chain, + GtkCallback callback, + gpointer user_data, + GDestroyNotify notify) +{ + DzlShortcutClosureChain *tail; + + g_return_val_if_fail (callback != NULL, NULL); + + tail = dzl_shortcut_closure_chain_new (DZL_SHORTCUT_CLOSURE_CALLBACK); + tail->callback.callback = callback; + tail->callback.user_data = user_data; + tail->callback.notify = notify; + + return dzl_shortcut_closure_chain_append (chain, tail); +} + +DzlShortcutClosureChain * +dzl_shortcut_closure_chain_append_command (DzlShortcutClosureChain *chain, + const gchar *command) +{ + DzlShortcutClosureChain *tail; + + g_return_val_if_fail (command != NULL, NULL); + + tail = dzl_shortcut_closure_chain_new (DZL_SHORTCUT_CLOSURE_COMMAND); + tail->command.name = g_intern_string (command); + + return dzl_shortcut_closure_chain_append (chain, tail); +} + +DzlShortcutClosureChain * +dzl_shortcut_closure_chain_append_action (DzlShortcutClosureChain *chain, + const gchar *group_name, + const gchar *action_name, + GVariant *params) +{ + DzlShortcutClosureChain *tail; + + g_return_val_if_fail (group_name != NULL, NULL); + g_return_val_if_fail (action_name != NULL, NULL); + + tail = dzl_shortcut_closure_chain_new (DZL_SHORTCUT_CLOSURE_ACTION); + tail->action.group = g_intern_string (group_name); + tail->action.name = g_intern_string (action_name); + tail->action.params = params ? g_variant_ref_sink (params) : NULL; + + return dzl_shortcut_closure_chain_append (chain, tail); +} + +DzlShortcutClosureChain * +dzl_shortcut_closure_chain_append_action_string (DzlShortcutClosureChain *chain, + const gchar *detailed_action_name) +{ + DzlShortcutClosureChain *tail; + g_autoptr(GVariant) target_value = NULL; + g_autofree gchar *prefix = NULL; + g_autofree gchar *name = NULL; + + g_return_val_if_fail (detailed_action_name != NULL, NULL); + + if (!dzl_g_action_name_parse_full (detailed_action_name, &prefix, &name, &target_value)) + { + g_warning ("Failed to parse action: %s", detailed_action_name); + return NULL; + } + + tail = dzl_shortcut_closure_chain_new (DZL_SHORTCUT_CLOSURE_ACTION); + tail->action.group = g_intern_string (prefix); + tail->action.name = g_intern_string (name); + tail->action.params = g_steal_pointer (&target_value); + + return dzl_shortcut_closure_chain_append (chain, tail); +} + +DzlShortcutClosureChain * +dzl_shortcut_closure_chain_append_signalv (DzlShortcutClosureChain *chain, + const gchar *signal_name, + GArray *params) +{ + g_autofree gchar *truncated_name = NULL; + DzlShortcutClosureChain *tail; + g_autoptr(GArray) copy = NULL; + const gchar *detail_str; + GQuark detail = 0; + + g_return_val_if_fail (signal_name != NULL, NULL); + + if (params != NULL) + { + copy = g_array_sized_new (FALSE, TRUE, sizeof (GValue), params->len); + g_array_set_clear_func (copy, (GDestroyNotify)g_value_unset); + g_array_set_size (copy, params->len); + + for (guint i = 0; i < params->len; i++) + { + GValue *src = &g_array_index (params, GValue, i); + GValue *dst = &g_array_index (copy, GValue, i); + + g_value_init (dst, G_VALUE_TYPE (src)); + g_value_copy (src, dst); + } + } + + if (NULL != (detail_str = strstr (signal_name, "::"))) + { + truncated_name = g_strndup (signal_name, detail_str - signal_name); + signal_name = truncated_name; + detail_str = &detail_str[2]; + detail = g_quark_try_string (detail_str); + } + + tail = dzl_shortcut_closure_chain_new (DZL_SHORTCUT_CLOSURE_SIGNAL); + tail->signal.name = g_intern_string (signal_name); + tail->signal.params = g_steal_pointer (©); + tail->signal.detail = detail; + + return dzl_shortcut_closure_chain_append (chain, tail); +} + +DzlShortcutClosureChain * +dzl_shortcut_closure_chain_append_signal (DzlShortcutClosureChain *chain, + const gchar *signal_name, + guint n_args, + va_list args) +{ + g_autoptr(GArray) params = NULL; + + g_return_val_if_fail (signal_name != NULL, NULL); + + params = g_array_new (FALSE, FALSE, sizeof (GValue)); + g_array_set_clear_func (params, (GDestroyNotify)g_value_unset); + + for (; n_args > 0; n_args--) + { + g_autofree gchar *errstr = NULL; + GValue value = { 0 }; + GType type; + + type = va_arg (args, GType); + + G_VALUE_COLLECT_INIT (&value, type, args, 0, &errstr); + + if (errstr != NULL) + { + g_warning ("%s", errstr); + break; + } + + g_array_append_val (params, value); + } + + return dzl_shortcut_closure_chain_append_signalv (chain, signal_name, params); +} + +static gboolean +find_instance_and_signal (GtkWidget *widget, + const gchar *signal_name, + gpointer *instance, + GSignalQuery *query) +{ + DzlShortcutController *controller; + + g_assert (GTK_IS_WIDGET (widget)); + g_assert (signal_name != NULL); + g_assert (instance != NULL); + g_assert (query != NULL); + + *instance = NULL; + + /* + * First we want to see if we can resolve the signal on the widgets + * controller (if there is one). This allows us to change contexts + * from signals without installing signals on the actual widgets. + */ + + controller = dzl_shortcut_controller_try_find (widget); + + if (controller != NULL) + { + guint signal_id; + + signal_id = g_signal_lookup (signal_name, G_OBJECT_TYPE (controller)); + + if (signal_id != 0) + { + g_signal_query (signal_id, query); + *instance = controller; + return TRUE; + } + } + + /* + * This diverts from Gtk signal keybindings a bit in that we + * allow you to activate a signal on any widget in the focus + * hierarchy starting from the provided widget up. + */ + + while (widget != NULL) + { + guint signal_id; + + signal_id = g_signal_lookup (signal_name, G_OBJECT_TYPE (widget)); + + if (signal_id != 0) + { + g_signal_query (signal_id, query); + *instance = widget; + return TRUE; + } + + widget = gtk_widget_get_parent (widget); + } + + return FALSE; +} + +static gboolean +signal_activate (DzlShortcutClosureChain *chain, + GtkWidget *widget) +{ + GValue *params; + GValue return_value = { 0 }; + GSignalQuery query; + gpointer instance = NULL; + + g_assert (chain != NULL); + g_assert (chain->type == DZL_SHORTCUT_CLOSURE_SIGNAL); + g_assert (GTK_IS_WIDGET (widget)); + + if (!find_instance_and_signal (widget, chain->signal.name, &instance, &query)) + { + g_warning ("Failed to locate signal %s in hierarchy of %s", + chain->signal.name, G_OBJECT_TYPE_NAME (widget)); + return TRUE; + } + + if (query.n_params != chain->signal.params->len) + goto parameter_mismatch; + + for (guint i = 0; i < query.n_params; i++) + { + if (!G_VALUE_HOLDS (&g_array_index (chain->signal.params, GValue, i), query.param_types[i])) + goto parameter_mismatch; + } + + params = g_new0 (GValue, 1 + query.n_params); + g_value_init_from_instance (¶ms[0], instance); + for (guint i = 0; i < query.n_params; i++) + { + GValue *src_value = &g_array_index (chain->signal.params, GValue, i); + + g_value_init (¶ms[1+i], G_VALUE_TYPE (src_value)); + g_value_copy (src_value, ¶ms[1+i]); + } + + if (query.return_type != G_TYPE_NONE) + g_value_init (&return_value, query.return_type); + + g_signal_emitv (params, query.signal_id, chain->signal.detail, &return_value); + + for (guint i = 0; i < query.n_params + 1; i++) + g_value_unset (¶ms[i]); + g_free (params); + + if (query.return_type != G_TYPE_NONE) + g_value_unset (&return_value); + + return GDK_EVENT_STOP; + +parameter_mismatch: + g_warning ("The parameters are not correct for signal %s", + chain->signal.name); + + /* + * If there was a bug with the signal descriptor, we still want + * to swallow the event to keep it from propagating further. + */ + + return GDK_EVENT_STOP; +} + +static gboolean +command_activate (DzlShortcutClosureChain *chain, + GtkWidget *widget) +{ + g_assert (chain != NULL); + g_assert (GTK_IS_WIDGET (widget)); + + for (; widget != NULL; widget = gtk_widget_get_parent (widget)) + { + DzlShortcutController *controller = dzl_shortcut_controller_try_find (widget); + + if (controller != NULL) + { + if (dzl_shortcut_controller_execute_command (controller, chain->command.name)) + return TRUE; + } + } + + g_warning ("Failed to locate controller command: %s", chain->command.name); + + return FALSE; +} + +gboolean +dzl_shortcut_closure_chain_execute (DzlShortcutClosureChain *chain, + GtkWidget *widget) +{ + gboolean ret = FALSE; + + DZL_ENTRY; + + g_return_val_if_fail (chain != NULL, FALSE); + g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE); + + if (chain->executing) + { + g_warning ("Attempt for re-entrancy in closure chain activation blocked"); + DZL_RETURN (FALSE); + } + + chain->executing = TRUE; + + switch (chain->type) + { + case DZL_SHORTCUT_CLOSURE_ACTION: + DZL_TRACE_MSG ("executing closure action %s.%s", + chain->action.group, chain->action.name); + ret |= dzl_gtk_widget_action (widget, + chain->action.group, + chain->action.name, + chain->action.params); + break; + + case DZL_SHORTCUT_CLOSURE_CALLBACK: + DZL_TRACE_MSG ("executing closure callback"); + chain->callback.callback (widget, chain->callback.user_data); + ret = TRUE; + break; + + case DZL_SHORTCUT_CLOSURE_SIGNAL: + DZL_TRACE_MSG ("executing closure signal"); + ret |= signal_activate (chain, widget); + break; + + case DZL_SHORTCUT_CLOSURE_COMMAND: + DZL_TRACE_MSG ("executing closure command \"%s\"", chain->command.name); + ret |= command_activate (chain, widget); + break; + + case DZL_SHORTCUT_CLOSURE_LAST: + default: + g_warning ("Unknown closure type"); + break; + } + + if (chain->node.next != NULL) + ret |= dzl_shortcut_closure_chain_execute (chain->node.next->data, widget); + + chain->executing = FALSE; + + DZL_TRACE_MSG ("ret = %d", ret); + + DZL_RETURN (ret); +} diff --git a/src/shortcuts/dzl-shortcut-closure-chain.h b/src/shortcuts/dzl-shortcut-closure-chain.h new file mode 100644 index 0000000..3d1e1da --- /dev/null +++ b/src/shortcuts/dzl-shortcut-closure-chain.h @@ -0,0 +1,55 @@ +/* dzl-shortcut-closure-chain.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_SHORTCUT_CLOSURE_CHAIN_H +#define DZL_SHORTCUT_CLOSURE_CHAIN_H + +#include + +G_BEGIN_DECLS + +typedef struct _DzlShortcutClosureChain DzlShortcutClosureChain; + +DzlShortcutClosureChain *dzl_shortcut_closure_chain_append (DzlShortcutClosureChain *chain, + DzlShortcutClosureChain *link); +DzlShortcutClosureChain *dzl_shortcut_closure_chain_append_signal (DzlShortcutClosureChain *chain, + const gchar *signal_name, + guint n_args, + va_list args); +DzlShortcutClosureChain *dzl_shortcut_closure_chain_append_signalv (DzlShortcutClosureChain *chain, + const gchar *signal_name, + GArray *params); +DzlShortcutClosureChain *dzl_shortcut_closure_chain_append_action (DzlShortcutClosureChain *chain, + const gchar *group_name, + const gchar *action_name, + GVariant *params); +DzlShortcutClosureChain *dzl_shortcut_closure_chain_append_action_string (DzlShortcutClosureChain *chain, + const gchar *detailed_action_name); +DzlShortcutClosureChain *dzl_shortcut_closure_chain_append_command (DzlShortcutClosureChain *chain, + const gchar *command); +DzlShortcutClosureChain *dzl_shortcut_closure_chain_append_callback (DzlShortcutClosureChain *chain, + GtkCallback callback, + gpointer user_data, + GDestroyNotify notify); +gboolean dzl_shortcut_closure_chain_execute (DzlShortcutClosureChain *chain, + GtkWidget *widget); +void dzl_shortcut_closure_chain_free (DzlShortcutClosureChain *chain); + +G_END_DECLS + +#endif /* DZL_SHORTCUT_CLOSURE_CHAIN_H */ diff --git a/src/shortcuts/dzl-shortcut-context.c b/src/shortcuts/dzl-shortcut-context.c new file mode 100644 index 0000000..ec48a16 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-context.c @@ -0,0 +1,514 @@ +/* dzl-shortcut-context.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-context" + +#include "config.h" + +#include +#include + +#include "dzl-debug.h" + +#include "shortcuts/dzl-shortcut-chord.h" +#include "shortcuts/dzl-shortcut-closure-chain.h" +#include "shortcuts/dzl-shortcut-context.h" +#include "shortcuts/dzl-shortcut-controller.h" +#include "shortcuts/dzl-shortcut-private.h" + +typedef struct +{ + /* The name of the context, interned */ + const gchar *name; + + /* The table of entries in this context which maps to a shortcut. + * These need to be copied across when merging down to another + * context layer. + */ + DzlShortcutChordTable *table; + + /* If we should use binding sets. By default this is true, but + * we use a signed 2-bit int for -1 being "unset". That allows + * us to know when the value was set on a layer and merge that + * value upwards. + */ + gint use_binding_sets : 2; +} DzlShortcutContextPrivate; + +enum { + PROP_0, + PROP_NAME, + PROP_USE_BINDING_SETS, + N_PROPS +}; + +struct _DzlShortcutContext +{ + GObject parent_instance; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlShortcutContext, dzl_shortcut_context, G_TYPE_OBJECT) + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_shortcut_context_finalize (GObject *object) +{ + DzlShortcutContext *self = (DzlShortcutContext *)object; + DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self); + + g_clear_pointer (&priv->table, dzl_shortcut_chord_table_free); + + G_OBJECT_CLASS (dzl_shortcut_context_parent_class)->finalize (object); +} + +static void +dzl_shortcut_context_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutContext *self = (DzlShortcutContext *)object; + DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self); + + switch (prop_id) + { + case PROP_NAME: + g_value_set_string (value, priv->name); + break; + + case PROP_USE_BINDING_SETS: + g_value_set_boolean (value, !!priv->use_binding_sets); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_context_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutContext *self = (DzlShortcutContext *)object; + DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self); + + switch (prop_id) + { + case PROP_NAME: + priv->name = g_intern_string (g_value_get_string (value)); + break; + + case PROP_USE_BINDING_SETS: + priv->use_binding_sets = g_value_get_boolean (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_context_class_init (DzlShortcutContextClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_shortcut_context_finalize; + object_class->get_property = dzl_shortcut_context_get_property; + object_class->set_property = dzl_shortcut_context_set_property; + + properties [PROP_NAME] = + g_param_spec_string ("name", + "Name", + "Name", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_USE_BINDING_SETS] = + g_param_spec_boolean ("use-binding-sets", + "Use Binding Sets", + "If the context should allow activation using binding sets", + TRUE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_shortcut_context_init (DzlShortcutContext *self) +{ + DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self); + + priv->use_binding_sets = -1; +} + +DzlShortcutContext * +dzl_shortcut_context_new (const gchar *name) +{ + return g_object_new (DZL_TYPE_SHORTCUT_CONTEXT, + "name", name, + NULL); +} + +const gchar * +dzl_shortcut_context_get_name (DzlShortcutContext *self) +{ + DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), NULL); + + return priv->name; +} + +gboolean +_dzl_shortcut_context_contains (DzlShortcutContext *self, + const DzlShortcutChord *chord) +{ + DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self); + gpointer data; + + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), FALSE); + g_return_val_if_fail (chord != NULL, FALSE); + + return priv->table != NULL && + dzl_shortcut_chord_table_lookup (priv->table, chord, &data) == DZL_SHORTCUT_MATCH_EQUAL; +} + +DzlShortcutMatch +dzl_shortcut_context_activate (DzlShortcutContext *self, + GtkWidget *widget, + const DzlShortcutChord *chord) +{ + DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self); + DzlShortcutMatch match = DZL_SHORTCUT_MATCH_NONE; + DzlShortcutClosureChain *chain = NULL; + + DZL_ENTRY; + + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), DZL_SHORTCUT_MATCH_NONE); + g_return_val_if_fail (GTK_IS_WIDGET (widget), DZL_SHORTCUT_MATCH_NONE); + g_return_val_if_fail (chord != NULL, DZL_SHORTCUT_MATCH_NONE); + + if (priv->table == NULL) + DZL_RETURN (DZL_SHORTCUT_MATCH_NONE); + +#if 0 + g_print ("Looking up %s in table %p (of size %u)\n", + dzl_shortcut_chord_to_string (chord), + priv->table, + dzl_shortcut_chord_table_size (priv->table)); + + dzl_shortcut_chord_table_printf (priv->table); +#endif + + match = dzl_shortcut_chord_table_lookup (priv->table, chord, (gpointer *)&chain); + + if (match == DZL_SHORTCUT_MATCH_EQUAL) + { + g_assert (chain != NULL); + + /* + * If we got a full match, but it failed to activate, we could potentially + * have another partial match. However, that lands squarely in the land of + * undefined behavior. So instead we just assume there was no match. + */ + if (!dzl_shortcut_closure_chain_execute (chain, widget)) + match = DZL_SHORTCUT_MATCH_NONE; + } + + DZL_TRACE_MSG ("%s: match = %d", priv->name, match); + + DZL_RETURN (match); +} + +static void +dzl_shortcut_context_add (DzlShortcutContext *self, + const DzlShortcutChord *chord, + DzlShortcutClosureChain *chain) +{ + DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self); + DzlShortcutClosureChain *head = NULL; + DzlShortcutMatch match; + + g_assert (DZL_IS_SHORTCUT_CONTEXT (self)); + g_assert (chord != NULL); + g_assert (chain != NULL); + + if (priv->table == NULL) + { + priv->table = dzl_shortcut_chord_table_new (); + dzl_shortcut_chord_table_set_free_func (priv->table, + (GDestroyNotify)dzl_shortcut_closure_chain_free); + } + + /* + * If we find that there is another entry for this shortcut, we chain onto + * the end of that item. This allows us to call multiple signals, or + * interleave signals and actions. + */ + + match = dzl_shortcut_chord_table_lookup (priv->table, chord, (gpointer *)&head); + + if (match == DZL_SHORTCUT_MATCH_EQUAL) + dzl_shortcut_closure_chain_append (head, chain); + else + dzl_shortcut_chord_table_add (priv->table, chord, chain); +} + +void +dzl_shortcut_context_add_action (DzlShortcutContext *self, + const gchar *accel, + const gchar *detailed_action_name) +{ + g_autoptr(DzlShortcutChord) chord = NULL; + DzlShortcutClosureChain *chain; + + g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (self)); + g_return_if_fail (accel != NULL); + g_return_if_fail (detailed_action_name != NULL); + + chord = dzl_shortcut_chord_new_from_string (accel); + + if (chord == NULL) + { + g_warning ("Failed to parse accelerator “%s”", accel); + return; + } + + chain = dzl_shortcut_closure_chain_append_action_string (NULL, detailed_action_name); + + dzl_shortcut_context_add (self, chord, chain); +} + +void +dzl_shortcut_context_add_command (DzlShortcutContext *self, + const gchar *accel, + const gchar *command) +{ + g_autoptr(DzlShortcutChord) chord = NULL; + DzlShortcutClosureChain *chain; + + g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (self)); + g_return_if_fail (accel != NULL); + g_return_if_fail (command != NULL); + + chord = dzl_shortcut_chord_new_from_string (accel); + + if (chord == NULL) + { + g_warning ("Failed to parse accelerator “%s” for command “%s”", + accel, command); + return; + } + + chain = dzl_shortcut_closure_chain_append_command (NULL, command); + + dzl_shortcut_context_add (self, chord, chain); +} + +void +dzl_shortcut_context_add_signal_va_list (DzlShortcutContext *self, + const gchar *accel, + const gchar *signal_name, + guint n_args, + va_list args) +{ + g_autoptr(DzlShortcutChord) chord = NULL; + DzlShortcutClosureChain *chain; + + g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (self)); + g_return_if_fail (accel != NULL); + g_return_if_fail (signal_name != NULL); + + chord = dzl_shortcut_chord_new_from_string (accel); + + if (chord == NULL) + { + g_warning ("Failed to parse accelerator \"%s\"", accel); + return; + } + + chain = dzl_shortcut_closure_chain_append_signal (NULL, signal_name, n_args, args); + + dzl_shortcut_context_add (self, chord, chain); +} + +void +dzl_shortcut_context_add_signal (DzlShortcutContext *self, + const gchar *accel, + const gchar *signal_name, + guint n_args, + ...) +{ + va_list args; + + va_start (args, n_args); + dzl_shortcut_context_add_signal_va_list (self, accel, signal_name, n_args, args); + va_end (args); +} + +/** + * dzl_shortcut_context_add_signalv: + * @self: a #DzlShortcutContext + * @accel: the accelerator for the shortcut + * @signal_name: the name of the signal + * @values: (element-type GObject.Value) (nullable) (transfer none): The + * values to use when calling the signal. + * + * This is similar to dzl_shortcut_context_add_signal() but is easier to use + * from language bindings. + */ +void +dzl_shortcut_context_add_signalv (DzlShortcutContext *self, + const gchar *accel, + const gchar *signal_name, + GArray *values) +{ + g_autoptr(DzlShortcutChord) chord = NULL; + DzlShortcutClosureChain *chain; + + g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (self)); + g_return_if_fail (accel != NULL); + g_return_if_fail (signal_name != NULL); + + chord = dzl_shortcut_chord_new_from_string (accel); + + if (chord == NULL) + { + g_warning ("Failed to parse accelerator \"%s\"", accel); + return; + } + + chain = dzl_shortcut_closure_chain_append_signalv (NULL, signal_name, values); + + dzl_shortcut_context_add (self, chord, chain); +} + +gboolean +dzl_shortcut_context_remove (DzlShortcutContext *self, + const gchar *accel) +{ + DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self); + g_autoptr(DzlShortcutChord) chord = NULL; + + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), FALSE); + g_return_val_if_fail (accel != NULL, FALSE); + + chord = dzl_shortcut_chord_new_from_string (accel); + + if (chord != NULL && priv->table != NULL) + return dzl_shortcut_chord_table_remove (priv->table, chord); + + return FALSE; +} + +gboolean +dzl_shortcut_context_load_from_data (DzlShortcutContext *self, + const gchar *data, + gssize len, + GError **error) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + + if (len < 0) + len = strlen (data); + + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Failed to parse shortcut data"); + + return FALSE; +} + +gboolean +dzl_shortcut_context_load_from_resource (DzlShortcutContext *self, + const gchar *resource_path, + GError **error) +{ + g_autoptr(GBytes) bytes = NULL; + const gchar *endptr = NULL; + const gchar *data; + gsize len; + + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), FALSE); + + if (NULL == (bytes = g_resources_lookup_data (resource_path, 0, error))) + return FALSE; + + data = g_bytes_get_data (bytes, &len); + + if (!g_utf8_validate (data, len, &endptr)) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Invalid UTF-8 at offset %u", + (guint)(endptr - data)); + return FALSE; + } + + return dzl_shortcut_context_load_from_data (self, data, len, error); +} + +DzlShortcutChordTable * +_dzl_shortcut_context_get_table (DzlShortcutContext *self) +{ + DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), NULL); + + return priv->table; +} + +void +_dzl_shortcut_context_merge (DzlShortcutContext *self, + DzlShortcutContext *layer) +{ + DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self); + DzlShortcutContextPrivate *layer_priv = dzl_shortcut_context_get_instance_private (layer); + DzlShortcutChordTableIter iter; + const DzlShortcutChord *chord; + gpointer value; + + g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (self)); + g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (layer)); + g_return_if_fail (layer != self); + + if (layer_priv->use_binding_sets != -1) + priv->use_binding_sets = layer_priv->use_binding_sets; + + _dzl_shortcut_chord_table_iter_init (&iter, layer_priv->table); + + while (_dzl_shortcut_chord_table_iter_next (&iter, &chord, &value)) + { + DzlShortcutClosureChain *chain = value; + + /* Make sure this doesn't exist in the base layer anymore */ + dzl_shortcut_chord_table_remove (priv->table, chord); + + /* Now add it to our table of chords */ + dzl_shortcut_context_add (self, chord, chain); + + /* Now we can safely steal this from the upper layer */ + _dzl_shortcut_chord_table_iter_steal (&iter); + } +} diff --git a/src/shortcuts/dzl-shortcut-context.h b/src/shortcuts/dzl-shortcut-context.h new file mode 100644 index 0000000..20215b2 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-context.h @@ -0,0 +1,83 @@ +/* dzl-shortcut-context.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 2 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 . + */ + +#ifndef DZL_SHORTCUT_CONTEXT_H +#define DZL_SHORTCUT_CONTEXT_H + +#include + +#include "dzl-version-macros.h" + +#include "dzl-shortcut-chord.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUT_CONTEXT (dzl_shortcut_context_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlShortcutContext, dzl_shortcut_context, DZL, SHORTCUT_CONTEXT, GObject) + +DZL_AVAILABLE_IN_ALL +DzlShortcutContext *dzl_shortcut_context_new (const gchar *name); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_shortcut_context_get_name (DzlShortcutContext *self); +DZL_AVAILABLE_IN_ALL +DzlShortcutMatch dzl_shortcut_context_activate (DzlShortcutContext *self, + GtkWidget *widget, + const DzlShortcutChord *chord); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_context_add_action (DzlShortcutContext *self, + const gchar *accel, + const gchar *detailed_action_name); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_context_add_command (DzlShortcutContext *self, + const gchar *accel, + const gchar *command); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_context_add_signal (DzlShortcutContext *self, + const gchar *accel, + const gchar *signal_name, + guint n_args, + ...); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_context_add_signal_va_list (DzlShortcutContext *self, + const gchar *accel, + const gchar *signal_name, + guint n_args, + va_list args); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_context_add_signalv (DzlShortcutContext *self, + const gchar *accel, + const gchar *signal_name, + GArray *values); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_context_remove (DzlShortcutContext *self, + const gchar *accel); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_context_load_from_data (DzlShortcutContext *self, + const gchar *data, + gssize len, + GError **error); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_context_load_from_resource (DzlShortcutContext *self, + const gchar *resource_path, + GError **error); + +G_END_DECLS + +#endif /* DZL_SHORTCUT_CONTEXT_H */ diff --git a/src/shortcuts/dzl-shortcut-controller.c b/src/shortcuts/dzl-shortcut-controller.c new file mode 100644 index 0000000..826b042 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-controller.c @@ -0,0 +1,1234 @@ +/* dzl-shortcut-controller.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 2 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-controller" + +#include "config.h" + +#include +#include + +#include "dzl-debug.h" + +#include "shortcuts/dzl-shortcut-closure-chain.h" +#include "shortcuts/dzl-shortcut-context.h" +#include "shortcuts/dzl-shortcut-controller.h" +#include "shortcuts/dzl-shortcut-manager.h" +#include "shortcuts/dzl-shortcut-private.h" + +typedef struct +{ + /* + * This is the widget for which we are the shortcut controller. There are + * zero or one shortcut controller for a given widget. These are persistent + * and dispatch events to the current DzlShortcutContext (which can be + * changed upon theme changes or shortcuts emitting the ::set-context signal. + */ + GtkWidget *widget; + + /* + * This is the name of the current context. Contexts are resolved at runtime + * by locating them within the theme (or inherited theme). They are interned + * strings to avoid lots of allocations between widgets. + */ + const gchar *context_name; + + /* + * If we are building a chord, it will be tracked here. Each incoming + * GdkEventKey will contribute to the creation of this chord. + */ + DzlShortcutChord *current_chord; + + /* + * This is a pointer to the root controller for the window. We register with + * the root controller so that keybindings can be activated even when the + * focus widget is somewhere else. + */ + DzlShortcutController *root; + + /* + * The commands that are attached to this controller including callbacks, + * signals, or actions. We use the commands_table to get a chord to the + * intern'd string containing the command id (for direct comparisons). + */ + GHashTable *commands; + + /* + * The command table is used to provide a mapping from accelerator/chord + * to the key for @commands. The data for each chord is an interned string + * which can be used as a direct pointer for lookups in @commands. + */ + DzlShortcutChordTable *commands_table; + + /* + * The root controller may have a manager associated with it to determine + * what themes and shortcuts are available. + */ + DzlShortcutManager *manager; + + /* + * The root controller keeps track of the children controllers in the window. + * Instead of allocating GList entries, we use an inline GList for the Queue + * link nodes. + */ + GQueue descendants; + + /* + * To avoid allocating GList nodes for controllers, we just inline a link + * here and attach it to @descendants when necessary. + */ + GList descendants_link; + + /* Signal handlers to react to various changes in the system. */ + gulong hierarchy_changed_handler; + gulong widget_destroy_handler; + gulong manager_changed_handler; + + /* If we have any global shortcuts registered */ + guint have_global : 1; +} DzlShortcutControllerPrivate; + +enum { + PROP_0, + PROP_CONTEXT, + PROP_CURRENT_CHORD, + PROP_MANAGER, + PROP_WIDGET, + N_PROPS +}; + +enum { + RESET, + SET_CONTEXT_NAMED, + N_SIGNALS +}; + +struct _DzlShortcutController { GObject object; }; +G_DEFINE_TYPE_WITH_PRIVATE (DzlShortcutController, dzl_shortcut_controller, G_TYPE_OBJECT) + +static GParamSpec *properties [N_PROPS]; +static guint signals [N_SIGNALS]; +static GQuark root_quark; +static GQuark controller_quark; + +static void dzl_shortcut_controller_connect (DzlShortcutController *self); +static void dzl_shortcut_controller_disconnect (DzlShortcutController *self); + +static void +dzl_shortcut_controller_emit_reset (DzlShortcutController *self) +{ + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + + g_signal_emit (self, signals[RESET], 0); +} + +static inline gboolean +dzl_shortcut_controller_is_root (DzlShortcutController *self) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + return priv->root == NULL; +} + +/** + * dzl_shortcut_controller_get_manager: + * @self: a #DzlShortcutController + * + * Gets the #DzlShortcutManager associated with this controller. + * + * Generally, this will look for the root controller's manager as mixing and + * matching managers in a single window hierarchy is not supported. + * + * Returns: (not nullable) (transfer none): A #DzlShortcutManager. + */ +DzlShortcutManager * +dzl_shortcut_controller_get_manager (DzlShortcutController *self) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + + if (priv->root != NULL) + return dzl_shortcut_controller_get_manager (priv->root); + + if (priv->manager != NULL) + return priv->manager; + + return dzl_shortcut_manager_get_default (); +} + +/** + * dzl_shortcut_controller_set_manager: + * @self: a #DzlShortcutController + * @manager: (nullable): A #DzlShortcutManager or %NULL + * + * Sets the #DzlShortcutController:manager property. + * + * If you set this to %NULL, it will revert to the default #DzlShortcutManager + * for the process. + */ +void +dzl_shortcut_controller_set_manager (DzlShortcutController *self, + DzlShortcutManager *manager) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_return_if_fail (!manager || DZL_IS_SHORTCUT_MANAGER (manager)); + + if (g_set_object (&priv->manager, manager)) + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MANAGER]); +} + +static gboolean +dzl_shortcut_controller_is_mapped (DzlShortcutController *self) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + return priv->widget != NULL && gtk_widget_get_mapped (priv->widget); +} + +static void +dzl_shortcut_controller_add (DzlShortcutController *self, + DzlShortcutController *descendant) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + DzlShortcutControllerPrivate *dpriv = dzl_shortcut_controller_get_instance_private (descendant); + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_assert (DZL_IS_SHORTCUT_CONTROLLER (descendant)); + + g_object_ref (descendant); + + if (dzl_shortcut_controller_is_mapped (descendant)) + g_queue_push_head_link (&priv->descendants, &dpriv->descendants_link); + else + g_queue_push_tail_link (&priv->descendants, &dpriv->descendants_link); +} + +static void +dzl_shortcut_controller_remove (DzlShortcutController *self, + DzlShortcutController *descendant) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + DzlShortcutControllerPrivate *dpriv = dzl_shortcut_controller_get_instance_private (descendant); + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_assert (DZL_IS_SHORTCUT_CONTROLLER (descendant)); + + g_queue_unlink (&priv->descendants, &dpriv->descendants_link); + g_object_unref (descendant); +} + +static void +dzl_shortcut_controller_on_manager_changed (DzlShortcutController *self, + DzlShortcutManager *manager) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_assert (DZL_IS_SHORTCUT_MANAGER (manager)); + + priv->context_name = NULL; + _dzl_shortcut_controller_clear (self); + dzl_shortcut_controller_emit_reset (self); +} + +static void +dzl_shortcut_controller_widget_destroy (DzlShortcutController *self, + GtkWidget *widget) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_assert (GTK_IS_WIDGET (widget)); + + dzl_shortcut_controller_disconnect (self); + g_clear_weak_pointer (&priv->widget); + + if (priv->root != NULL) + { + dzl_shortcut_controller_remove (priv->root, self); + g_clear_object (&priv->root); + } +} + +static void +dzl_shortcut_controller_widget_hierarchy_changed (DzlShortcutController *self, + GtkWidget *previous_toplevel, + GtkWidget *widget) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + GtkWidget *toplevel; + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_assert (!previous_toplevel || GTK_IS_WIDGET (previous_toplevel)); + g_assert (GTK_IS_WIDGET (widget)); + + /* + * We attach our controller to the root controller if we have shortcuts in + * the global activation phase. That allows the bubble/capture phase to + * potentially dispatch to our controller. + */ + + g_object_ref (self); + + if (priv->root != NULL) + { + dzl_shortcut_controller_remove (priv->root, self); + g_clear_object (&priv->root); + } + + if (priv->have_global) + { + toplevel = gtk_widget_get_toplevel (widget); + + if (toplevel != widget) + { + priv->root = g_object_get_qdata (G_OBJECT (toplevel), root_quark); + if (priv->root == NULL) + priv->root = dzl_shortcut_controller_new (toplevel); + dzl_shortcut_controller_add (priv->root, self); + } + } + + g_object_unref (self); +} + +static void +dzl_shortcut_controller_disconnect (DzlShortcutController *self) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + DzlShortcutManager *manager; + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_assert (GTK_IS_WIDGET (priv->widget)); + + manager = dzl_shortcut_controller_get_manager (self); + + g_signal_handler_disconnect (priv->widget, priv->widget_destroy_handler); + priv->widget_destroy_handler = 0; + + g_signal_handler_disconnect (priv->widget, priv->hierarchy_changed_handler); + priv->hierarchy_changed_handler = 0; + + g_signal_handler_disconnect (manager, priv->manager_changed_handler); + priv->manager_changed_handler = 0; +} + +static void +dzl_shortcut_controller_connect (DzlShortcutController *self) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + DzlShortcutManager *manager; + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_assert (GTK_IS_WIDGET (priv->widget)); + + manager = dzl_shortcut_controller_get_manager (self); + + g_clear_pointer (&priv->current_chord, dzl_shortcut_chord_free); + priv->context_name = NULL; + + priv->widget_destroy_handler = + g_signal_connect_swapped (priv->widget, + "destroy", + G_CALLBACK (dzl_shortcut_controller_widget_destroy), + self); + + priv->hierarchy_changed_handler = + g_signal_connect_swapped (priv->widget, + "hierarchy-changed", + G_CALLBACK (dzl_shortcut_controller_widget_hierarchy_changed), + self); + + priv->manager_changed_handler = + g_signal_connect_swapped (manager, + "changed", + G_CALLBACK (dzl_shortcut_controller_on_manager_changed), + self); + + dzl_shortcut_controller_widget_hierarchy_changed (self, NULL, priv->widget); +} + +static void +dzl_shortcut_controller_set_widget (DzlShortcutController *self, + GtkWidget *widget) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_assert (GTK_IS_WIDGET (widget)); + + if (widget != priv->widget) + { + if (priv->widget != NULL) + { + dzl_shortcut_controller_disconnect (self); + g_object_remove_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget); + priv->widget = NULL; + } + + if (widget != NULL && widget != priv->widget) + { + priv->widget = widget; + g_object_add_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget); + dzl_shortcut_controller_connect (self); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WIDGET]); + } +} + +/** + * dzl_shortcut_controller_set_context_by_name: + * @self: a #DzlShortcutController + * @name: (nullable): The name of the context + * + * Changes the context for the controller to the context matching @name. + * + * Contexts are resolved at runtime through the current theme (and possibly + * a parent theme if it inherits from one). + * + * Since: 3.26 + */ +void +dzl_shortcut_controller_set_context_by_name (DzlShortcutController *self, + const gchar *name) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self)); + + name = g_intern_string (name); + + if (name != priv->context_name) + { + priv->context_name = name; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONTEXT]); + dzl_shortcut_controller_emit_reset (self); + } +} + +static void +dzl_shortcut_controller_real_set_context_named (DzlShortcutController *self, + const gchar *name) +{ + g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self)); + + dzl_shortcut_controller_set_context_by_name (self, name); +} + +static void +dzl_shortcut_controller_finalize (GObject *object) +{ + DzlShortcutController *self = (DzlShortcutController *)object; + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + if (priv->widget != NULL) + { + g_object_remove_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget); + priv->widget = NULL; + } + + g_clear_pointer (&priv->commands, g_hash_table_unref); + g_clear_pointer (&priv->commands_table, dzl_shortcut_chord_table_free); + g_clear_object (&priv->root); + + while (priv->descendants.length > 0) + g_queue_unlink (&priv->descendants, priv->descendants.head); + + priv->context_name = NULL; + + G_OBJECT_CLASS (dzl_shortcut_controller_parent_class)->finalize (object); +} + +static void +dzl_shortcut_controller_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutController *self = (DzlShortcutController *)object; + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + switch (prop_id) + { + case PROP_CONTEXT: + g_value_set_object (value, dzl_shortcut_controller_get_context (self)); + break; + + case PROP_CURRENT_CHORD: + g_value_set_boxed (value, dzl_shortcut_controller_get_current_chord (self)); + break; + + case PROP_MANAGER: + g_value_set_object (value, dzl_shortcut_controller_get_manager (self)); + break; + + case PROP_WIDGET: + g_value_set_object (value, priv->widget); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_controller_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutController *self = (DzlShortcutController *)object; + + switch (prop_id) + { + case PROP_MANAGER: + dzl_shortcut_controller_set_manager (self, g_value_get_object (value)); + break; + + case PROP_WIDGET: + dzl_shortcut_controller_set_widget (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_controller_class_init (DzlShortcutControllerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_shortcut_controller_finalize; + object_class->get_property = dzl_shortcut_controller_get_property; + object_class->set_property = dzl_shortcut_controller_set_property; + + properties [PROP_CURRENT_CHORD] = + g_param_spec_boxed ("current-chord", + "Current Chord", + "The current chord for the controller", + DZL_TYPE_SHORTCUT_CHORD, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_CONTEXT] = + g_param_spec_object ("context", + "Context", + "The current context of the controller, for dispatch phase", + DZL_TYPE_SHORTCUT_CONTEXT, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_MANAGER] = + g_param_spec_object ("manager", + "Manager", + "The shortcut manager", + DZL_TYPE_SHORTCUT_MANAGER, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_WIDGET] = + g_param_spec_object ("widget", + "Widget", + "The widget for which the controller attached", + GTK_TYPE_WIDGET, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + /** + * DzlShortcutController::reset: + * + * This signal is emitted when the shortcut controller is requesting + * the widget to reset any state it may have regarding the shortcut + * controller. Such an example might be a modal system that lives + * outside the controller whose state should be cleared in response + * to the controller changing modes. + */ + signals [RESET] = + g_signal_new_class_handler ("reset", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + NULL, NULL, NULL, NULL, G_TYPE_NONE, 0); + + /** + * DzlShortcutController::set-context-named: + * @self: An #DzlShortcutController + * @name: The name of the context + * + * This changes the current context on the #DzlShortcutController to be the + * context matching @name. This is found by looking up the context by name + * in the active #DzlShortcutTheme. + */ + signals [SET_CONTEXT_NAMED] = + g_signal_new_class_handler ("set-context-named", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_CALLBACK (dzl_shortcut_controller_real_set_context_named), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_STRING); + + controller_quark = g_quark_from_static_string ("DZL_SHORTCUT_CONTROLLER"); + root_quark = g_quark_from_static_string ("DZL_SHORTCUT_CONTROLLER_ROOT"); +} + +static void +dzl_shortcut_controller_init (DzlShortcutController *self) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + g_queue_init (&priv->descendants); + + priv->descendants_link.data = self; +} + +DzlShortcutController * +dzl_shortcut_controller_new (GtkWidget *widget) +{ + DzlShortcutController *ret; + + g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL); + + if (NULL != (ret = g_object_get_qdata (G_OBJECT (widget), controller_quark))) + return g_object_ref (ret); + + ret = g_object_new (DZL_TYPE_SHORTCUT_CONTROLLER, + "widget", widget, + NULL); + + g_object_set_qdata_full (G_OBJECT (widget), + controller_quark, + g_object_ref (ret), + g_object_unref); + + return ret; +} + +/** + * dzl_shortcut_controller_try_find: + * + * Finds the registered #DzlShortcutController for a widget. + * + * If no controller is found, %NULL is returned. + * + * Returns: (nullable) (transfer none): An #DzlShortcutController or %NULL. + */ +DzlShortcutController * +dzl_shortcut_controller_try_find (GtkWidget *widget) +{ + g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL); + + return g_object_get_qdata (G_OBJECT (widget), controller_quark); +} + +/** + * dzl_shortcut_controller_find: + * + * Finds the registered #DzlShortcutController for a widget. + * + * The controller is created if it does not already exist. + * + * Returns: (not nullable) (transfer none): An #DzlShortcutController or %NULL. + */ +DzlShortcutController * +dzl_shortcut_controller_find (GtkWidget *widget) +{ + DzlShortcutController *controller; + + g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL); + + controller = g_object_get_qdata (G_OBJECT (widget), controller_quark); + + if (controller == NULL) + { + /* We want to pass a borrowed reference */ + g_object_unref (dzl_shortcut_controller_new (widget)); + controller = g_object_get_qdata (G_OBJECT (widget), controller_quark); + } + + return controller; +} + +static DzlShortcutContext * +_dzl_shortcut_controller_get_context_for_phase (DzlShortcutController *self, + DzlShortcutTheme *theme, + DzlShortcutPhase phase) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + g_autofree gchar *phased_name = NULL; + DzlShortcutContext *ret; + const gchar *name; + + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), NULL); + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (theme), NULL); + + if (priv->widget == NULL) + return NULL; + + name = priv->context_name ? priv->context_name : G_OBJECT_TYPE_NAME (priv->widget); + + g_return_val_if_fail (name != NULL, NULL); + + /* If we are in dispatch phase, we use our direct context */ + if (phase == DZL_SHORTCUT_PHASE_BUBBLE) + name = phased_name = g_strdup_printf ("%s:bubble", name); + else if (phase == DZL_SHORTCUT_PHASE_CAPTURE) + name = phased_name = g_strdup_printf ("%s:capture", name); + + ret = _dzl_shortcut_theme_try_find_context_by_name (theme, name); + + g_return_val_if_fail (!ret || DZL_IS_SHORTCUT_CONTEXT (ret), NULL); + + return ret; +} + +/** + * dzl_shortcut_controller_get_context_for_phase: + * @self: a #DzlShortcutController + * @phase: the phase for the shorcut delivery + * + * Controllers can have a different context for a particular phase, which allows + * them to activate different keybindings depending if the event in capture, + * bubble, or dispatch. + * + * Returns: (transfer none) (nullable): A #DzlShortcutContext or %NULL. + * + * Since: 3.26 + */ +DzlShortcutContext * +dzl_shortcut_controller_get_context_for_phase (DzlShortcutController *self, + DzlShortcutPhase phase) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + DzlShortcutManager *manager; + DzlShortcutTheme *theme; + + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), NULL); + + if (NULL == priv->widget || + NULL == (manager = dzl_shortcut_controller_get_manager (self)) || + NULL == (theme = dzl_shortcut_manager_get_theme (manager))) + return NULL; + + return _dzl_shortcut_controller_get_context_for_phase (self, theme, phase); +} + +/** + * dzl_shortcut_controller_get_context: + * @self: An #DzlShortcutController + * + * This function gets the #DzlShortcutController:context property, which + * is the current context to dispatch events to. An #DzlShortcutContext + * is a group of keybindings that may be activated in response to a + * single or series of #GdkEventKey. + * + * Returns: (transfer none) (nullable): A #DzlShortcutContext or %NULL. + * + * Since: 3.26 + */ +DzlShortcutContext * +dzl_shortcut_controller_get_context (DzlShortcutController *self) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), NULL); + + return dzl_shortcut_controller_get_context_for_phase (self, DZL_SHORTCUT_PHASE_DISPATCH); +} + +static DzlShortcutContext * +dzl_shortcut_controller_get_inherited_context (DzlShortcutController *self, + DzlShortcutPhase phase) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + DzlShortcutManager *manager; + DzlShortcutContext *ret; + DzlShortcutTheme *theme; + DzlShortcutTheme *parent; + const gchar *parent_name = NULL; + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + + if (NULL == priv->widget || + NULL == (manager = dzl_shortcut_controller_get_manager (self)) || + NULL == (theme = dzl_shortcut_manager_get_theme (manager)) || + NULL == (parent_name = dzl_shortcut_theme_get_parent_name (theme)) || + NULL == (parent = dzl_shortcut_manager_get_theme_by_name (manager, parent_name))) + return NULL; + + ret = _dzl_shortcut_controller_get_context_for_phase (self, parent, phase); + + g_return_val_if_fail (!ret || DZL_IS_SHORTCUT_CONTEXT (ret), NULL); + + return ret; +} + +static DzlShortcutMatch +dzl_shortcut_controller_process (DzlShortcutController *self, + const DzlShortcutChord *chord, + DzlShortcutPhase phase) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + DzlShortcutContext *context; + DzlShortcutMatch match = DZL_SHORTCUT_MATCH_NONE; + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_assert (chord != NULL); + + /* Try to activate our current context */ + if (match == DZL_SHORTCUT_MATCH_NONE && + NULL != (context = dzl_shortcut_controller_get_context_for_phase (self, phase))) + match = dzl_shortcut_context_activate (context, priv->widget, chord); + + /* If we didn't get a match, locate the context within the parent theme */ + if (match == DZL_SHORTCUT_MATCH_NONE && + NULL != (context = dzl_shortcut_controller_get_inherited_context (self, phase))) + match = dzl_shortcut_context_activate (context, priv->widget, chord); + + return match; +} + +static void +dzl_shortcut_controller_do_global_chain (DzlShortcutController *self, + DzlShortcutClosureChain *chain, + GtkWidget *widget, + GList *next) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_assert (chain != NULL); + g_assert (GTK_IS_WIDGET (widget)); + + /* + * If this is an action chain, the best we're going to be able to do is + * activate the action from the current widget. For commands, we can try to + * resolve them by locating commands within our registered controllers. + */ + + if (chain->type != DZL_SHORTCUT_CLOSURE_COMMAND) + { + dzl_shortcut_closure_chain_execute (chain, widget); + return; + } + + if (priv->commands != NULL && + g_hash_table_contains (priv->commands, chain->command.name)) + { + dzl_shortcut_closure_chain_execute (chain, priv->widget); + return; + } + + if (next == NULL) + { + dzl_shortcut_closure_chain_execute (chain, widget); + return; + } + + dzl_shortcut_controller_do_global_chain (next->data, chain, widget, next->next); +} + +static DzlShortcutMatch +dzl_shortcut_controller_do_global (DzlShortcutController *self, + const DzlShortcutChord *chord, + DzlShortcutPhase phase, + GtkWidget *widget) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + DzlShortcutClosureChain *chain = NULL; + DzlShortcutManager *manager; + DzlShortcutTheme *theme; + DzlShortcutMatch match; + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_assert (chord != NULL); + g_assert ((phase & DZL_SHORTCUT_PHASE_GLOBAL) != 0); + g_assert (GTK_IS_WIDGET (widget)); + + manager = dzl_shortcut_controller_get_manager (self); + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + + theme = dzl_shortcut_manager_get_theme (manager); + g_assert (DZL_IS_SHORTCUT_THEME (theme)); + + /* See if we have a chain for this chord */ + match = _dzl_shortcut_theme_match (theme, phase, chord, &chain); + + /* If we matched, execute the chain, trying to locate the proper widget for + * the event delivery. + */ + if (match == DZL_SHORTCUT_MATCH_EQUAL && chain->phase == phase) + dzl_shortcut_controller_do_global_chain (self, chain, widget, priv->descendants.head); + + return match; +} + +/** + * _dzl_shortcut_controller_handle: + * @self: An #DzlShortcutController + * @event: A #GdkEventKey + * @chord: the current chord for the toplevel + * @phase: the dispatch phase + * @widget: the widget receiving @event + * + * This function uses @event to determine if the current context has a shortcut + * registered matching the event. If so, the shortcut will be dispatched and + * %TRUE is returned. Otherwise, %FALSE is returned. + * + * @chord is used to track the current chord from the toplevel. Chord tracking + * is done in a single place to avoid inconsistencies between controllers. + * + * @phase should indicate the phase of the event dispatch. Capture is used + * to capture events before the destination #GdkWindow can process them, and + * bubble is to allow the destination window to handle it before processing + * the result afterwards if not yet handled. + * + * Returns: A #DzlShortcutMatch based on if the event was dispatched. + * + * Since: 3.26 + */ +DzlShortcutMatch +_dzl_shortcut_controller_handle (DzlShortcutController *self, + const GdkEventKey *event, + const DzlShortcutChord *chord, + DzlShortcutPhase phase, + GtkWidget *widget) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + DzlShortcutMatch match = DZL_SHORTCUT_MATCH_NONE; + + DZL_ENTRY; + + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + g_return_val_if_fail (chord != NULL, FALSE); + g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE); + + /* Nothing to do if the widget isn't visible/mapped/etc */ + if (priv->widget == NULL || + !gtk_widget_get_visible (priv->widget) || + !gtk_widget_get_child_visible (priv->widget) || + !gtk_widget_is_sensitive (priv->widget)) + DZL_RETURN (DZL_SHORTCUT_MATCH_NONE); + + DZL_TRACE_MSG ("widget = %s, phase = %d", G_OBJECT_TYPE_NAME (priv->widget), phase); + + /* Try to dispatch our capture global shortcuts first */ + if (phase == (DZL_SHORTCUT_PHASE_CAPTURE | DZL_SHORTCUT_PHASE_GLOBAL) && + dzl_shortcut_controller_is_root (self)) + match = dzl_shortcut_controller_do_global (self, chord, phase, widget); + + /* + * This function processes a particular phase for the event. If our phase + * is DZL_SHORTCUT_PHASE_CAPTURE, that means we are in the process of working + * our way from the toplevel down to the widget containing the event window. + * + * If our phase is DZL_SHORTCUT_PHASE_BUBBLE, then we are working our way + * up from the widget containing the event window to the toplevel. This is + * the phase where most activations should occur. + * + * During the capture phase, we look for a context matching the current + * context, but with a suffix on the context name like ":capture". So for + * the default GtkEntry, the capture context name would be something like + * "GtkEntry:capture". The bubble phase does not have a suffix. + * + * Toplevel Global Capture Accels + * Toplevel Capture + * - Child 1 Capture + * - Grandchild 1 Capture + * - Grandchild 1 Bubble + * - Child 1 Bubble + * Toplevel Bubble + * Toplevel Global Bubble Accels + * + * If we come across a keybinding that is a partial match, we assume that + * is the closest match in the dispatch chain and stop processing further. + * Overlapping and conflicting keybindings are considered undefined behavior + * and this falls under such a situation. + * + * Note that we do not perform the bubble/capture phase here, that is handled + * by our caller in DzlShortcutManager. + */ + + if (match == DZL_SHORTCUT_MATCH_NONE) + match = dzl_shortcut_controller_process (self, chord, phase); + + /* Try to dispatch our capture global shortcuts first */ + if (match == DZL_SHORTCUT_MATCH_NONE && + dzl_shortcut_controller_is_root (self) && + phase == (DZL_SHORTCUT_PHASE_BUBBLE | DZL_SHORTCUT_PHASE_GLOBAL)) + match = dzl_shortcut_controller_do_global (self, chord, phase, widget); + + DZL_TRACE_MSG ("match = %s", + match == DZL_SHORTCUT_MATCH_NONE ? "none" : + match == DZL_SHORTCUT_MATCH_PARTIAL ? "partial" : "equal"); + + DZL_RETURN (match); +} + +/** + * dzl_shortcut_controller_get_current_chord: + * @self: a #DzlShortcutController + * + * This method gets the #DzlShortcutController:current-chord property. + * This is useful if you want to monitor in-progress chord building. + * + * Note that this value will only be valid on the controller for the + * toplevel widget (a #GtkWindow). Chords are not tracked at the + * individual widget controller level. + * + * Returns: (transfer none) (nullable): A #DzlShortcutChord or %NULL. + */ +const DzlShortcutChord * +dzl_shortcut_controller_get_current_chord (DzlShortcutController *self) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), NULL); + + return priv->current_chord; +} + +/** + * dzl_shortcut_controller_execute_command: + * @self: a #DzlShortcutController + * @command: the id of the command + * + * This method will locate and execute the command matching the id @command. + * + * If the command is not found, %FALSE is returned. + * + * Returns: %TRUE if the command was found and executed. + */ +gboolean +dzl_shortcut_controller_execute_command (DzlShortcutController *self, + const gchar *command) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), FALSE); + g_return_val_if_fail (command != NULL, FALSE); + + if (priv->commands != NULL) + { + DzlShortcutClosureChain *chain; + + chain = g_hash_table_lookup (priv->commands, g_intern_string (command)); + + if (chain != NULL) + return dzl_shortcut_closure_chain_execute (chain, priv->widget); + } + + for (const GList *iter = priv->descendants.head; iter != NULL; iter = iter->next) + { + DzlShortcutController *descendant = iter->data; + + if (dzl_shortcut_controller_execute_command (descendant, command)) + return TRUE; + } + + return FALSE; +} + +static void +dzl_shortcut_controller_add_command (DzlShortcutController *self, + const gchar *command_id, + const gchar *default_accel, + DzlShortcutPhase phase, + DzlShortcutClosureChain *chain) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + g_autoptr(DzlShortcutChord) chord = NULL; + DzlShortcutManager *manager; + DzlShortcutTheme *theme; + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_assert (command_id != NULL); + g_assert (chain != NULL); + + /* Always use interned strings for command ids */ + command_id = g_intern_string (command_id); + + /* + * Set the phase on the closure chain so we know what phase we are allowed + * to execute the chain within during capture/dispatch/bubble. There is no + * "global + dispatch" phase, so if global is set, default to bubble. + */ + if (phase == DZL_SHORTCUT_PHASE_GLOBAL) + phase |= DZL_SHORTCUT_PHASE_BUBBLE; + chain->phase = phase; + + /* Add the closure chain to our set of commands. */ + if (priv->commands == NULL) + priv->commands = g_hash_table_new_full (NULL, NULL, NULL, + (GDestroyNotify)dzl_shortcut_closure_chain_free); + g_hash_table_insert (priv->commands, (gpointer)command_id, chain); + + /* + * If this command can be executed in the global phase, we need to be + * sure that the root controller knows that we must be checked during + * global activation checks. + */ + if ((phase & DZL_SHORTCUT_PHASE_GLOBAL) != 0) + { + if (priv->have_global != TRUE) + { + priv->have_global = TRUE; + if (priv->widget != NULL) + dzl_shortcut_controller_widget_hierarchy_changed (self, NULL, priv->widget); + } + } + + /* If an accel was provided, we need to register it in various places */ + if (default_accel != NULL) + { + /* Make sure this is a valid accelerator */ + chord = dzl_shortcut_chord_new_from_string (default_accel); + + if (chord != NULL) + { + DzlShortcutContext *context; + + /* Add the chord to our chord table for lookups */ + if (priv->commands_table == NULL) + priv->commands_table = dzl_shortcut_chord_table_new (); + dzl_shortcut_chord_table_add (priv->commands_table, chord, (gpointer)command_id); + + /* Set the value in the theme so it can have overrides by users */ + manager = dzl_shortcut_controller_get_manager (self); + theme = _dzl_shortcut_manager_get_internal_theme (manager); + dzl_shortcut_theme_set_chord_for_command (theme, command_id, chord, phase); + + /* Hook things up into the default context */ + context = _dzl_shortcut_theme_find_default_context_with_phase (theme, priv->widget, phase); + if (!_dzl_shortcut_context_contains (context, chord)) + dzl_shortcut_context_add_command (context, default_accel, command_id); + } + else + g_warning ("\"%s\" is not a valid accelerator chord", default_accel); + } +} + +void +dzl_shortcut_controller_add_command_action (DzlShortcutController *self, + const gchar *command_id, + const gchar *default_accel, + DzlShortcutPhase phase, + const gchar *action) +{ + DzlShortcutClosureChain *chain; + + g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_return_if_fail (command_id != NULL); + + chain = dzl_shortcut_closure_chain_append_action_string (NULL, action); + dzl_shortcut_controller_add_command (self, command_id, default_accel, phase, chain); +} + +void +dzl_shortcut_controller_add_command_callback (DzlShortcutController *self, + const gchar *command_id, + const gchar *default_accel, + DzlShortcutPhase phase, + GtkCallback callback, + gpointer callback_data, + GDestroyNotify callback_data_destroy) +{ + DzlShortcutClosureChain *chain; + + g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_return_if_fail (command_id != NULL); + + chain = dzl_shortcut_closure_chain_append_callback (NULL, + callback, + callback_data, + callback_data_destroy); + + dzl_shortcut_controller_add_command (self, command_id, default_accel, phase, chain); +} + +void +dzl_shortcut_controller_add_command_signal (DzlShortcutController *self, + const gchar *command_id, + const gchar *default_accel, + DzlShortcutPhase phase, + const gchar *signal_name, + guint n_args, + ...) +{ + DzlShortcutClosureChain *chain; + va_list args; + + g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self)); + g_return_if_fail (command_id != NULL); + + va_start (args, n_args); + chain = dzl_shortcut_closure_chain_append_signal (NULL, signal_name, n_args, args); + va_end (args); + + dzl_shortcut_controller_add_command (self, command_id, default_accel, phase, chain); +} + +DzlShortcutChord * +_dzl_shortcut_controller_push (DzlShortcutController *self, + const GdkEventKey *event) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), NULL); + g_return_val_if_fail (event != NULL, NULL); + + /* + * Only the toplevel controller handling the event needs to determine + * the current "chord" as that state lives in the root controller only. + * + * So our first step is to determine the current chord, or if this input + * breaks further chord processing. + * + * We will use these chords during capture/dispatch/bubble later on. + */ + if (priv->current_chord == NULL) + { + /* Try to create a new chord starting with this key. + * current_chord may still be NULL after this. + */ + priv->current_chord = dzl_shortcut_chord_new_from_event (event); + } + else + { + if (!dzl_shortcut_chord_append_event (priv->current_chord, event)) + { + /* Failed to add the key to the chord, cancel */ + _dzl_shortcut_controller_clear (self); + return NULL; + } + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_CHORD]); + + return dzl_shortcut_chord_copy (priv->current_chord); +} + +void +_dzl_shortcut_controller_clear (DzlShortcutController *self) +{ + DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self); + + g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self)); + + g_clear_pointer (&priv->current_chord, dzl_shortcut_chord_free); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_CHORD]); +} diff --git a/src/shortcuts/dzl-shortcut-controller.h b/src/shortcuts/dzl-shortcut-controller.h new file mode 100644 index 0000000..4cccac7 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-controller.h @@ -0,0 +1,86 @@ +/* dzl-shortcut-controller.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 2 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 . + */ + +#ifndef DZL_SHORTCUT_CONTROLLER_H +#define DZL_SHORTCUT_CONTROLLER_H + +#include + +#include "dzl-version-macros.h" + +#include "shortcuts/dzl-shortcut-context.h" +#include "shortcuts/dzl-shortcut-manager.h" +#include "shortcuts/dzl-shortcut-phase.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUT_CONTROLLER (dzl_shortcut_controller_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlShortcutController, dzl_shortcut_controller, DZL, SHORTCUT_CONTROLLER, GObject) + +DZL_AVAILABLE_IN_ALL +DzlShortcutController *dzl_shortcut_controller_new (GtkWidget *widget); +DZL_AVAILABLE_IN_ALL +DzlShortcutManager *dzl_shortcut_controller_get_manager (DzlShortcutController *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_controller_set_manager (DzlShortcutController *self, + DzlShortcutManager *manager); +DZL_AVAILABLE_IN_ALL +DzlShortcutController *dzl_shortcut_controller_find (GtkWidget *widget); +DZL_AVAILABLE_IN_ALL +DzlShortcutController *dzl_shortcut_controller_try_find (GtkWidget *widget); +DZL_AVAILABLE_IN_ALL +DzlShortcutContext *dzl_shortcut_controller_get_context (DzlShortcutController *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_controller_set_context_by_name (DzlShortcutController *self, + const gchar *name); +DZL_AVAILABLE_IN_ALL +DzlShortcutContext *dzl_shortcut_controller_get_context_for_phase (DzlShortcutController *self, + DzlShortcutPhase phase); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_controller_execute_command (DzlShortcutController *self, + const gchar *command); +DZL_AVAILABLE_IN_ALL +const DzlShortcutChord *dzl_shortcut_controller_get_current_chord (DzlShortcutController *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_controller_add_command_action (DzlShortcutController *self, + const gchar *command_id, + const gchar *default_accel, + DzlShortcutPhase phase, + const gchar *action); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_controller_add_command_callback (DzlShortcutController *self, + const gchar *command_id, + const gchar *default_accel, + DzlShortcutPhase phase, + GtkCallback callback, + gpointer callback_data, + GDestroyNotify callback_data_destroy); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_controller_add_command_signal (DzlShortcutController *self, + const gchar *command_id, + const gchar *default_accel, + DzlShortcutPhase phase, + const gchar *signal_name, + guint n_args, + ...); + +G_END_DECLS + +#endif /* DZL_SHORTCUT_CONTROLLER_H */ diff --git a/src/shortcuts/dzl-shortcut-label.c b/src/shortcuts/dzl-shortcut-label.c new file mode 100644 index 0000000..2b4aac6 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-label.c @@ -0,0 +1,222 @@ +/* dzl-shortcut-label.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-label" + +#include "config.h" + +#include "shortcuts/dzl-shortcut-label.h" + +struct _DzlShortcutLabel +{ + GtkBox parent_instance; + DzlShortcutChord *chord; +}; + +enum { + PROP_0, + PROP_ACCELERATOR, + PROP_CHORD, + N_PROPS +}; + +G_DEFINE_TYPE (DzlShortcutLabel, dzl_shortcut_label, GTK_TYPE_BOX) + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_shortcut_label_finalize (GObject *object) +{ + DzlShortcutLabel *self = (DzlShortcutLabel *)object; + + g_clear_pointer (&self->chord, dzl_shortcut_chord_free); + + G_OBJECT_CLASS (dzl_shortcut_label_parent_class)->finalize (object); +} + +static void +dzl_shortcut_label_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutLabel *self = DZL_SHORTCUT_LABEL (object); + + switch (prop_id) + { + case PROP_ACCELERATOR: + g_value_take_string (value, dzl_shortcut_label_get_accelerator (self)); + break; + + case PROP_CHORD: + g_value_set_boxed (value, dzl_shortcut_label_get_chord (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_label_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutLabel *self = DZL_SHORTCUT_LABEL (object); + + switch (prop_id) + { + case PROP_ACCELERATOR: + dzl_shortcut_label_set_accelerator (self, g_value_get_string (value)); + break; + + case PROP_CHORD: + dzl_shortcut_label_set_chord (self, g_value_get_boxed (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_label_class_init (DzlShortcutLabelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_shortcut_label_finalize; + object_class->get_property = dzl_shortcut_label_get_property; + object_class->set_property = dzl_shortcut_label_set_property; + + properties [PROP_ACCELERATOR] = + g_param_spec_string ("accelerator", + "Accelerator", + "The accelerator for the label", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_CHORD] = + g_param_spec_boxed ("chord", + "Chord", + "The chord for the label", + DZL_TYPE_SHORTCUT_CHORD, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_shortcut_label_init (DzlShortcutLabel *self) +{ + gtk_box_set_spacing (GTK_BOX (self), 12); +} + +GtkWidget * +dzl_shortcut_label_new (void) +{ + return g_object_new (DZL_TYPE_SHORTCUT_LABEL, NULL); +} + +gchar * +dzl_shortcut_label_get_accelerator (DzlShortcutLabel *self) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_LABEL (self), NULL); + + if (self->chord == NULL) + return NULL; + + return dzl_shortcut_chord_to_string (self->chord); +} + +void +dzl_shortcut_label_set_accelerator (DzlShortcutLabel *self, + const gchar *accelerator) +{ + g_autoptr(DzlShortcutChord) chord = NULL; + + g_return_if_fail (DZL_IS_SHORTCUT_LABEL (self)); + + if (accelerator != NULL) + chord = dzl_shortcut_chord_new_from_string (accelerator); + + dzl_shortcut_label_set_chord (self, chord); +} + +void +dzl_shortcut_label_set_chord (DzlShortcutLabel *self, + const DzlShortcutChord *chord) +{ + g_return_if_fail (DZL_IS_SHORTCUT_LABEL (self)); + + if (!dzl_shortcut_chord_equal (chord, self->chord)) + { + dzl_shortcut_chord_free (self->chord); + self->chord = dzl_shortcut_chord_copy (chord); + + gtk_container_foreach (GTK_CONTAINER (self), (GtkCallback) gtk_widget_destroy, NULL); + + if (chord != NULL) + { + GdkModifierType first_mod = 0; + guint len; + + len = dzl_shortcut_chord_get_length (chord); + + dzl_shortcut_chord_get_nth_key (chord, 0, NULL, &first_mod); + + for (guint i = 0; i < len; i++) + { + g_autofree gchar *accel = NULL; + GtkWidget *label; + GdkModifierType mod = 0; + guint keyval = 0; + + dzl_shortcut_chord_get_nth_key (chord, i, &keyval, &mod); + + if (i > 0 && (mod & first_mod) == first_mod) + accel = gtk_accelerator_name (keyval, mod & ~first_mod); + else + accel = gtk_accelerator_name (keyval, mod); + + label = g_object_new (GTK_TYPE_SHORTCUT_LABEL, + "accelerator", accel, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (self), label); + } + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACCELERATOR]); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHORD]); + } +} + +/** + * dzl_shortcut_label_get_chord: + * @self: a #DzlShortcutLabel + * + * Gets the chord for the label, or %NULL. + * + * Returns: (transfer none) (nullable): A #DzlShortcutChord or %NULL + */ +const DzlShortcutChord * +dzl_shortcut_label_get_chord (DzlShortcutLabel *self) +{ + return self->chord; +} diff --git a/src/shortcuts/dzl-shortcut-label.h b/src/shortcuts/dzl-shortcut-label.h new file mode 100644 index 0000000..c96dd5a --- /dev/null +++ b/src/shortcuts/dzl-shortcut-label.h @@ -0,0 +1,50 @@ +/* dzl-shortcut-label.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#ifndef DZL_SHORTCUT_LABEL_H +#define DZL_SHORTCUT_LABEL_H + +#include + +#include "dzl-version-macros.h" + +#include "dzl-shortcut-chord.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUT_LABEL (dzl_shortcut_label_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlShortcutLabel, dzl_shortcut_label, DZL, SHORTCUT_LABEL, GtkBox) + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_shortcut_label_new (void); +DZL_AVAILABLE_IN_ALL +gchar *dzl_shortcut_label_get_accelerator (DzlShortcutLabel *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_label_set_accelerator (DzlShortcutLabel *self, + const gchar *accelerator); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_label_set_chord (DzlShortcutLabel *self, + const DzlShortcutChord *chord); +DZL_AVAILABLE_IN_ALL +const DzlShortcutChord *dzl_shortcut_label_get_chord (DzlShortcutLabel *self); + +G_END_DECLS + +#endif /* DZL_SHORTCUT_LABEL_H */ diff --git a/src/shortcuts/dzl-shortcut-manager.c b/src/shortcuts/dzl-shortcut-manager.c new file mode 100644 index 0000000..7a4cf33 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-manager.c @@ -0,0 +1,1624 @@ +/* dzl-shortcut-manager.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 2 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-manager.h" + +#include "config.h" + +#include + +#include "dzl-debug.h" + +#include "shortcuts/dzl-shortcut-controller.h" +#include "shortcuts/dzl-shortcut-label.h" +#include "shortcuts/dzl-shortcut-manager.h" +#include "shortcuts/dzl-shortcut-private.h" +#include "shortcuts/dzl-shortcut-private.h" +#include "shortcuts/dzl-shortcuts-group.h" +#include "shortcuts/dzl-shortcuts-section.h" +#include "shortcuts/dzl-shortcuts-shortcut.h" +#include "util/dzl-gtk.h" +#include "util/dzl-util-private.h" + +typedef struct +{ + /* + * This is the currently selected theme by the user (or default until + * a theme has been set). You can change this with the + * dzl_shortcut_manager_set_theme() function. + */ + DzlShortcutTheme *theme; + + /* + * To avoid re-implementing lots of behavior, we use an internal theme + * to store all the built-in keybindings for shortcut controllers. Then, + * when loading themes (particularly default), we copy these into that + * theme to give the effect of inheritance. + */ + DzlShortcutTheme *internal_theme; + + /* + * This is an array of all of the themes owned by the manager. It does + * not, however, contain the @internal_theme instance. + */ + GPtrArray *themes; + + /* + * This is the user directory to save changes to the theme so they can + * be reloaded later. + */ + gchar *user_dir; + + /* + * To simplify the process of registering entries, we allow them to be + * called from the instance init function. But we only want to see those + * entries once. If we did this from class_init(), we'd run into issues + * with gtk not being initialized yet (and we need access to keymaps). + * + * This allows us to keep a unique pointer to know if we've already + * dealt with some entries by discarding them up front. + */ + GHashTable *seen_entries; + + /* + * We store a tree of various shortcut data so that we can build the + * shortcut window using the registered controller actions. This is + * done in dzl_shortcut_manager_add_shortcuts_to_window(). + */ + GNode *root; + + /* + * We keep track of the search paths for loading themes here. Each element is + * a string containing the path to the file-system resource. If the path + * starts with 'resource://" it is assumed a resource embedded in the current + * process. + */ + GQueue search_path; + + /* + * Upon making changes to @search path, we need to reload the themes. This + * is a GSource identifier to indicate our queued reload request. + */ + guint reload_handler; +} DzlShortcutManagerPrivate; + +enum { + PROP_0, + PROP_THEME, + PROP_THEME_NAME, + PROP_USER_DIR, + N_PROPS +}; + +enum { + CHANGED, + N_SIGNALS +}; + +static void list_model_iface_init (GListModelInterface *iface); +static void initable_iface_init (GInitableIface *iface); +static void dzl_shortcut_manager_load_directory (DzlShortcutManager *self, + const gchar *resource_dir, + GCancellable *cancellable); +static void dzl_shortcut_manager_load_resources (DzlShortcutManager *self, + const gchar *resource_dir, + GCancellable *cancellable); +static void dzl_shortcut_manager_merge (DzlShortcutManager *self, + DzlShortcutTheme *theme); + +G_DEFINE_TYPE_WITH_CODE (DzlShortcutManager, dzl_shortcut_manager, G_TYPE_OBJECT, + G_ADD_PRIVATE (DzlShortcutManager) + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init) + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)) + +static GParamSpec *properties [N_PROPS]; +static guint signals [N_SIGNALS]; + +static gboolean +free_node_data (GNode *node, + gpointer user_data) +{ + DzlShortcutNodeData *data = node->data; + + g_slice_free (DzlShortcutNodeData, data); + + return FALSE; +} + +static void +destroy_theme (gpointer data) +{ + g_autoptr(DzlShortcutTheme) theme = data; + + g_assert (DZL_IS_SHORTCUT_THEME (theme)); + + _dzl_shortcut_theme_set_manager (theme, NULL); +} + +void +dzl_shortcut_manager_reload (DzlShortcutManager *self, + GCancellable *cancellable) +{ + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + g_autofree gchar *theme_name = NULL; + g_autofree gchar *parent_theme_name = NULL; + DzlShortcutTheme *theme = NULL; + guint previous_len; + + DZL_ENTRY; + + g_assert (DZL_IS_SHORTCUT_MANAGER (self)); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + DZL_TRACE_MSG ("reloading shortcuts, current theme is “%s”", + priv->theme ? dzl_shortcut_theme_get_name (priv->theme) : "internal"); + + /* + * If there is a queued reload when we get here, just remove it. When called + * from a queued callback, this will already be zeroed. + */ + if (priv->reload_handler != 0) + { + g_source_remove (priv->reload_handler); + priv->reload_handler = 0; + } + + if (priv->theme != NULL) + { + /* + * Keep a copy of the current theme name so that we can return to the + * same theme if it is still available. If it has disappeared, then we + * will try to fallback to the parent theme. + */ + theme_name = g_strdup (dzl_shortcut_theme_get_name (priv->theme)); + parent_theme_name = g_strdup (dzl_shortcut_theme_get_parent_name (priv->theme)); + _dzl_shortcut_theme_detach (priv->theme); + g_clear_object (&priv->theme); + } + + /* + * Now remove all of our old themes and notify listeners via the GListModel + * interface so things like preferences can update. We ensure that we place + * a "default" item in the list as we should always have one. We'll append to + * it when loading the default theme anyway. + * + * The default theme always inherits from internal so that we can store + * our widget/controller defined shortcuts separate from the mutable default + * theme which various applications might want to tweak in their overrides. + */ + previous_len = priv->themes->len; + g_ptr_array_remove_range (priv->themes, 0, previous_len); + g_ptr_array_add (priv->themes, g_object_new (DZL_TYPE_SHORTCUT_THEME, + "name", "default", + "title", _("Default Shortcuts"), + "parent-name", "internal", + NULL)); + _dzl_shortcut_theme_set_manager (g_ptr_array_index (priv->themes, 0), self); + g_list_model_items_changed (G_LIST_MODEL (self), 0, previous_len, 1); + + /* + * Okay, now we can go and load all the files in the search path. After + * loading a file, the loader code will call dzl_shortcut_manager_merge() + * to layer that theme into any base theme which matches the name. This + * allows application plugins to simply load a keytheme file to have it + * merged into the parent keytheme. + */ + for (const GList *iter = priv->search_path.tail; iter != NULL; iter = iter->prev) + { + const gchar *directory = iter->data; + + if (g_str_has_prefix (directory, "resource://")) + dzl_shortcut_manager_load_resources (self, directory, cancellable); + else + dzl_shortcut_manager_load_directory (self, directory, cancellable); + } + + DZL_TRACE_MSG ("Attempting to reset theme to %s", + theme_name ?: parent_theme_name ?: "internal"); + + /* Now try to reapply the same theme if we can find it. */ + if (theme_name != NULL) + { + theme = dzl_shortcut_manager_get_theme_by_name (self, theme_name); + if (theme != NULL) + dzl_shortcut_manager_set_theme (self, theme); + } + + if (priv->theme == NULL && parent_theme_name != NULL) + { + theme = dzl_shortcut_manager_get_theme_by_name (self, parent_theme_name); + if (theme != NULL) + dzl_shortcut_manager_set_theme (self, theme); + } + + /* Notify possibly changed properties */ + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME]); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME_NAME]); + + DZL_EXIT; +} + +static gboolean +dzl_shortcut_manager_do_reload (gpointer data) +{ + DzlShortcutManager *self = data; + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + g_assert (DZL_IS_SHORTCUT_MANAGER (self)); + + priv->reload_handler = 0; + dzl_shortcut_manager_reload (self, NULL); + return G_SOURCE_REMOVE; +} + +void +dzl_shortcut_manager_queue_reload (DzlShortcutManager *self) +{ + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + DZL_ENTRY; + + g_assert (DZL_IS_SHORTCUT_MANAGER (self)); + + if (priv->reload_handler == 0) + { + /* + * Reload at a high priority to happen immediately, but defer + * until getting to the main loop. + */ + priv->reload_handler = + gdk_threads_add_idle_full (G_PRIORITY_HIGH, + dzl_shortcut_manager_do_reload, + g_object_ref (self), + g_object_unref); + } + + DZL_EXIT; +} + +static void +dzl_shortcut_manager_finalize (GObject *object) +{ + DzlShortcutManager *self = (DzlShortcutManager *)object; + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + if (priv->root != NULL) + { + g_node_traverse (priv->root, G_IN_ORDER, G_TRAVERSE_ALL, -1, free_node_data, NULL); + g_node_destroy (priv->root); + priv->root = NULL; + } + + if (priv->theme != NULL) + { + _dzl_shortcut_theme_detach (priv->theme); + g_clear_object (&priv->theme); + } + + g_clear_pointer (&priv->seen_entries, g_hash_table_unref); + g_clear_pointer (&priv->themes, g_ptr_array_unref); + g_clear_pointer (&priv->user_dir, g_free); + g_clear_object (&priv->internal_theme); + + G_OBJECT_CLASS (dzl_shortcut_manager_parent_class)->finalize (object); +} + +static void +dzl_shortcut_manager_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutManager *self = (DzlShortcutManager *)object; + + switch (prop_id) + { + case PROP_THEME: + g_value_set_object (value, dzl_shortcut_manager_get_theme (self)); + break; + + case PROP_THEME_NAME: + g_value_set_string (value, dzl_shortcut_manager_get_theme_name (self)); + break; + + case PROP_USER_DIR: + g_value_set_string (value, dzl_shortcut_manager_get_user_dir (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_manager_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutManager *self = (DzlShortcutManager *)object; + + switch (prop_id) + { + case PROP_THEME: + dzl_shortcut_manager_set_theme (self, g_value_get_object (value)); + break; + + case PROP_THEME_NAME: + dzl_shortcut_manager_set_theme_name (self, g_value_get_string (value)); + break; + + case PROP_USER_DIR: + dzl_shortcut_manager_set_user_dir (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_manager_class_init (DzlShortcutManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_shortcut_manager_finalize; + object_class->get_property = dzl_shortcut_manager_get_property; + object_class->set_property = dzl_shortcut_manager_set_property; + + properties [PROP_THEME] = + g_param_spec_object ("theme", + "Theme", + "The current key theme.", + DZL_TYPE_SHORTCUT_THEME, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_THEME_NAME] = + g_param_spec_string ("theme-name", + "Theme Name", + "The name of the current theme", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_USER_DIR] = + g_param_spec_string ("user-dir", + "User Dir", + "The directory for saved user modifications", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + signals [CHANGED] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, G_TYPE_NONE, 0); +} + +static guint +shortcut_entry_hash (gconstpointer key) +{ + DzlShortcutEntry *entry = (DzlShortcutEntry *)key; + guint command_hash = 0; + guint section_hash = 0; + guint group_hash = 0; + guint title_hash = 0; + guint subtitle_hash = 0; + + if (entry->command != NULL) + command_hash = g_str_hash (entry->command); + + if (entry->section != NULL) + section_hash = g_str_hash (entry->section); + + if (entry->group != NULL) + group_hash = g_str_hash (entry->group); + + if (entry->title != NULL) + title_hash = g_str_hash (entry->title); + + if (entry->subtitle != NULL) + subtitle_hash = g_str_hash (entry->subtitle); + + return (command_hash ^ section_hash ^ group_hash ^ title_hash ^ subtitle_hash); +} + +static void +dzl_shortcut_manager_init (DzlShortcutManager *self) +{ + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + priv->seen_entries = g_hash_table_new (shortcut_entry_hash, NULL); + priv->themes = g_ptr_array_new_with_free_func (destroy_theme); + priv->root = g_node_new (NULL); + priv->internal_theme = g_object_new (DZL_TYPE_SHORTCUT_THEME, + "name", "internal", + NULL); +} + +static void +dzl_shortcut_manager_load_directory (DzlShortcutManager *self, + const gchar *directory, + GCancellable *cancellable) +{ + g_autoptr(GDir) dir = NULL; + const gchar *name; + + DZL_ENTRY; + + g_assert (DZL_IS_SHORTCUT_MANAGER (self)); + g_assert (directory != NULL); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + DZL_TRACE_MSG ("directory = %s", directory); + + if (!g_file_test (directory, G_FILE_TEST_IS_DIR)) + DZL_EXIT; + + if (NULL == (dir = g_dir_open (directory, 0, NULL))) + DZL_EXIT; + + while (NULL != (name = g_dir_read_name (dir))) + { + g_autofree gchar *path = g_build_filename (directory, name, NULL); + g_autoptr(DzlShortcutTheme) theme = NULL; + g_autoptr(GError) local_error = NULL; + + theme = dzl_shortcut_theme_new (NULL); + + if (dzl_shortcut_theme_load_from_path (theme, path, cancellable, &local_error)) + { + _dzl_shortcut_theme_set_manager (theme, self); + dzl_shortcut_manager_merge (self, theme); + } + else + g_warning ("%s", local_error->message); + } + + DZL_EXIT; +} + +static void +dzl_shortcut_manager_load_resources (DzlShortcutManager *self, + const gchar *resource_dir, + GCancellable *cancellable) +{ + g_auto(GStrv) children = NULL; + + DZL_ENTRY; + + g_assert (DZL_IS_SHORTCUT_MANAGER (self)); + g_assert (resource_dir != NULL); + g_assert (g_str_has_prefix (resource_dir, "resource://")); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + DZL_TRACE_MSG ("resource_dir = %s", resource_dir); + + if (g_str_has_prefix (resource_dir, "resource://")) + resource_dir += strlen ("resource://"); + + children = g_resources_enumerate_children (resource_dir, 0, NULL); + + if (children != NULL) + { + for (guint i = 0; children[i] != NULL; i++) + { + g_autofree gchar *path = g_build_path ("/", resource_dir, children[i], NULL); + g_autoptr(DzlShortcutTheme) theme = NULL; + g_autoptr(GError) local_error = NULL; + g_autoptr(GBytes) bytes = NULL; + const gchar *data; + gsize len = 0; + + if (NULL == (bytes = g_resources_lookup_data (path, 0, NULL))) + continue; + + data = g_bytes_get_data (bytes, &len); + theme = dzl_shortcut_theme_new (NULL); + + if (dzl_shortcut_theme_load_from_data (theme, data, len, &local_error)) + { + _dzl_shortcut_theme_set_manager (theme, self); + dzl_shortcut_manager_merge (self, theme); + } + else + g_warning ("%s", local_error->message); + } + } + + DZL_EXIT; +} + +static gboolean +dzl_shortcut_manager_initiable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + DzlShortcutManager *self = (DzlShortcutManager *)initable; + + g_assert (DZL_IS_SHORTCUT_MANAGER (self)); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + dzl_shortcut_manager_reload (self, cancellable); + + return TRUE; +} + +static void +initable_iface_init (GInitableIface *iface) +{ + iface->init = dzl_shortcut_manager_initiable_init; +} + +/** + * dzl_shortcut_manager_get_default: + * + * Gets the singleton #DzlShortcutManager for the process. + * + * Returns: (transfer none) (not nullable): An #DzlShortcutManager. + */ +DzlShortcutManager * +dzl_shortcut_manager_get_default (void) +{ + static DzlShortcutManager *instance; + + if (instance == NULL) + { + instance = g_object_new (DZL_TYPE_SHORTCUT_MANAGER, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); + } + + return instance; +} + +/** + * dzl_shortcut_manager_get_theme: + * @self: (nullable): A #DzlShortcutManager or %NULL + * + * Gets the "theme" property. + * + * Returns: (transfer none) (not nullable): An #DzlShortcutTheme. + */ +DzlShortcutTheme * +dzl_shortcut_manager_get_theme (DzlShortcutManager *self) +{ + DzlShortcutManagerPrivate *priv; + + g_return_val_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self), NULL); + + if (self == NULL) + self = dzl_shortcut_manager_get_default (); + + priv = dzl_shortcut_manager_get_instance_private (self); + + if G_LIKELY (priv->theme != NULL) + return priv->theme; + + for (guint i = 0; i < priv->themes->len; i++) + { + DzlShortcutTheme *theme = g_ptr_array_index (priv->themes, i); + + if (g_strcmp0 (dzl_shortcut_theme_get_name (theme), "default") == 0) + { + priv->theme = g_object_ref (theme); + return priv->theme; + } + } + + return priv->internal_theme; +} + +/** + * dzl_shortcut_manager_set_theme: + * @self: An #DzlShortcutManager + * @theme: (not nullable): An #DzlShortcutTheme + * + * Sets the theme for the shortcut manager. + */ +void +dzl_shortcut_manager_set_theme (DzlShortcutManager *self, + DzlShortcutTheme *theme) +{ + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + DZL_ENTRY; + + g_return_if_fail (DZL_IS_SHORTCUT_MANAGER (self)); + g_return_if_fail (DZL_IS_SHORTCUT_THEME (theme)); + + /* + * It is important that DzlShortcutController instances watch for + * notify::theme so that they can reset their state. Otherwise, we + * could be transitioning between incorrect contexts. + */ + + if (priv->theme != theme) + { + if (priv->theme != NULL) + { + _dzl_shortcut_theme_detach (priv->theme); + g_clear_object (&priv->theme); + } + + if (theme != NULL) + { + priv->theme = g_object_ref (theme); + _dzl_shortcut_theme_attach (priv->theme); + } + + DZL_TRACE_MSG ("theme set to “%s”", + theme ? dzl_shortcut_theme_get_name (theme) : "internal"); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME]); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME_NAME]); + } +} + +/* + * dzl_shortcut_manager_run_phase: + * @self: a #DzlShortcutManager + * @event: the event in question + * @chord: the current chord for the toplevel + * @phase: the phase (capture, bubble) + * @widget: the widget the event was destined for + * @focus: the current focus widget + * + * Runs a particular phase of the event dispatch. + * + * A phase can be either capture or bubble. Capture tries to deliver the + * event starting from the root down to the given widget. Bubble tries to + * deliver the event starting from the widget up to the toplevel. + * + * These two phases allow stealing before or after, depending on the needs + * of the keybindings. + * + * Returns: A #DzlShortcutMatch + */ +static DzlShortcutMatch +dzl_shortcut_manager_run_phase (DzlShortcutManager *self, + const GdkEventKey *event, + const DzlShortcutChord *chord, + int phase, + GtkWidget *widget, + GtkWidget *focus) +{ + GtkWidget *ancestor = widget; + GQueue queue = G_QUEUE_INIT; + DzlShortcutMatch ret = DZL_SHORTCUT_MATCH_NONE; + + g_assert (DZL_IS_SHORTCUT_MANAGER (self)); + g_assert (event != NULL); + g_assert (chord != NULL); + g_assert ((phase & DZL_SHORTCUT_PHASE_GLOBAL) == 0); + g_assert (GTK_IS_WIDGET (widget)); + g_assert (GTK_IS_WIDGET (focus)); + + /* + * Collect all the widgets that might be needed for this phase and order them + * so that we can process from first-to-last. Capture phase is + * toplevel-to-widget, and bubble is widget-to-toplevel. Dispatch only has + * the the widget itself. + */ + do + { + if (phase == DZL_SHORTCUT_PHASE_CAPTURE) + g_queue_push_head (&queue, g_object_ref (ancestor)); + else + g_queue_push_tail (&queue, g_object_ref (ancestor)); + ancestor = gtk_widget_get_parent (ancestor); + } + while (phase != DZL_SHORTCUT_PHASE_DISPATCH && ancestor != NULL); + + /* + * Now look through our widget chain to find a match to activate. + */ + for (const GList *iter = queue.head; iter; iter = iter->next) + { + GtkWidget *current = iter->data; + DzlShortcutController *controller; + + controller = dzl_shortcut_controller_try_find (current); + + if (controller != NULL) + { + /* + * Now try to activate the event using the controller. If we get + * any result other than DZL_SHORTCUT_MATCH_NONE, we need to stop + * processing and swallow the event. + * + * Multiple controllers can have a partial match, but if any hits + * a partial match, it's undefined behavior to also have a shortcut + * which would activate. + */ + ret = _dzl_shortcut_controller_handle (controller, event, chord, phase, focus); + if (ret) + DZL_GOTO (cleanup); + } + + /* + * If we are in the dispatch phase, we will only see our target widget for + * the event delivery. Try to dispatch the event and if so we consider + * the event handled. + */ + if (phase == DZL_SHORTCUT_PHASE_DISPATCH) + { + if (gtk_widget_event (current, (GdkEvent *)event)) + { + ret = DZL_SHORTCUT_MATCH_EQUAL; + DZL_GOTO (cleanup); + } + } + } + +cleanup: + g_queue_foreach (&queue, (GFunc)g_object_unref, NULL); + g_queue_clear (&queue); + + DZL_RETURN (ret); +} + +static DzlShortcutMatch +dzl_shortcut_manager_run_global (DzlShortcutManager *self, + const GdkEventKey *event, + const DzlShortcutChord *chord, + DzlShortcutPhase phase, + DzlShortcutController *root, + GtkWidget *widget) +{ + g_assert (DZL_IS_SHORTCUT_MANAGER (self)); + g_assert (event != NULL); + g_assert (chord != NULL); + g_assert (phase == DZL_SHORTCUT_PHASE_CAPTURE || + phase == DZL_SHORTCUT_PHASE_BUBBLE); + g_assert (DZL_IS_SHORTCUT_CONTROLLER (root)); + g_assert (GTK_WIDGET (widget)); + + /* + * The goal of this function is to locate a shortcut within any + * controller registered with the root controller (or the root + * controller itself) that is registered as a "global shortcut". + */ + + phase |= DZL_SHORTCUT_PHASE_GLOBAL; + + return _dzl_shortcut_controller_handle (root, event, chord, phase, widget); +} + +static gboolean +dzl_shortcut_manager_run_fallbacks (DzlShortcutManager *self, + GtkWidget *widget, + GtkWidget *toplevel, + const DzlShortcutChord *chord) +{ + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + g_assert (DZL_IS_SHORTCUT_MANAGER (self)); + g_assert (GTK_IS_WIDGET (widget)); + g_assert (GTK_IS_WIDGET (toplevel)); + g_assert (chord != NULL); + + if (dzl_shortcut_chord_get_length (chord) == 1) + { + GApplication *app = g_application_get_default (); + const gchar *action; + GdkModifierType state; + guint keyval; + + dzl_shortcut_chord_get_nth_key (chord, 0, &keyval, &state); + + /* See if the toplevel activates this, like Tab, etc */ + if (gtk_bindings_activate (G_OBJECT (toplevel), keyval, state)) + return TRUE; + + /* See if there is a mnemonic active that should be activated */ + if (GTK_IS_WINDOW (toplevel) && + gtk_window_mnemonic_activate (GTK_WINDOW (toplevel), keyval, state)) + return TRUE; + + /* + * See if we have something defined for this theme that + * can be activated directly. + */ + action = _dzl_shortcut_theme_lookup_action (priv->internal_theme, chord); + + if (action != NULL) + { + g_autofree gchar *prefix = NULL; + g_autofree gchar *name = NULL; + g_autoptr(GVariant) target = NULL; + + dzl_g_action_name_parse_full (action, &prefix, &name, &target); + + if (dzl_gtk_widget_action (toplevel, prefix, name, target)) + return TRUE; + } + + /* + * Now fallback to trying to activate the action within GtkApplication + * as the legacy Gtk bindings would do. + */ + + if (GTK_IS_APPLICATION (app)) + { + g_autofree gchar *accel = dzl_shortcut_chord_to_string (chord); + g_auto(GStrv) actions = NULL; + + actions = gtk_application_get_actions_for_accel (GTK_APPLICATION (app), accel); + + if (actions != NULL) + { + for (guint i = 0; actions[i] != NULL; i++) + { + g_autofree gchar *prefix = NULL; + g_autofree gchar *name = NULL; + g_autoptr(GVariant) param = NULL; + + action = actions[i]; + + if (!dzl_g_action_name_parse_full (action, &prefix, &name, ¶m)) + { + g_warning ("Failed to parse: %s", action); + continue; + } + + if (dzl_gtk_widget_action (widget, prefix, name, param)) + return TRUE; + } + } + } + } + + return FALSE; +} + +/** + * dzl_shortcut_manager_handle_event: + * @self: (nullable): An #DzlShortcutManager + * @toplevel: A #GtkWidget or %NULL. + * @event: A #GdkEventKey event to handle. + * + * This function will try to dispatch @event to the proper widget and + * #DzlShortcutContext. If the event is handled, then %TRUE is returned. + * + * You should call this from #GtkWidget::key-press-event handler in your + * #GtkWindow toplevel. + * + * Returns: %TRUE if the event was handled. + */ +gboolean +dzl_shortcut_manager_handle_event (DzlShortcutManager *self, + const GdkEventKey *event, + GtkWidget *toplevel) +{ + g_autoptr(DzlShortcutChord) chord = NULL; + DzlShortcutController *root; + DzlShortcutMatch match; + GtkWidget *widget; + GtkWidget *focus; + gboolean ret = GDK_EVENT_PROPAGATE; + + DZL_ENTRY; + + g_return_val_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self), FALSE); + g_return_val_if_fail (!toplevel || GTK_IS_WINDOW (toplevel), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + if (self == NULL) + self = dzl_shortcut_manager_get_default (); + + /* We don't support anything but key-press */ + if (event->type != GDK_KEY_PRESS) + DZL_RETURN (GDK_EVENT_PROPAGATE); + + /* We might need to discover our toplevel from the event */ + if (toplevel == NULL) + { + gpointer user_data; + + gdk_window_get_user_data (event->window, &user_data); + g_return_val_if_fail (GTK_IS_WIDGET (user_data), FALSE); + + toplevel = gtk_widget_get_toplevel (user_data); + g_return_val_if_fail (GTK_IS_WINDOW (toplevel), FALSE); + } + + /* Sanitiy checks */ + g_assert (DZL_IS_SHORTCUT_MANAGER (self)); + g_assert (GTK_IS_WINDOW (toplevel)); + g_assert (event != NULL); + + /* Synthesize focus as the toplevel if there is none */ + widget = focus = gtk_window_get_focus (GTK_WINDOW (toplevel)); + if (widget == NULL) + widget = focus = toplevel; + + /* + * We want to push this event into the toplevel controller. If it + * gives us back a chord, then we can try to dispatch that up/down + * the controller tree. + */ + root = dzl_shortcut_controller_find (toplevel); + chord = _dzl_shortcut_controller_push (root, event); + if (chord == NULL) + DZL_RETURN (GDK_EVENT_PROPAGATE); + +#ifdef DZL_ENABLE_TRACE + { + g_autofree gchar *str = dzl_shortcut_chord_to_string (chord); + DZL_TRACE_MSG ("current chord: %s", str); + } +#endif + + /* + * Now we have our chord/event to dispatch to the individual controllers + * on widgets. We can run through the phases to capture/dispatch/bubble. + */ + if ((match = dzl_shortcut_manager_run_global (self, event, chord, DZL_SHORTCUT_PHASE_CAPTURE, root, widget)) || + (match = dzl_shortcut_manager_run_phase (self, event, chord, DZL_SHORTCUT_PHASE_CAPTURE, widget, focus)) || + (match = dzl_shortcut_manager_run_phase (self, event, chord, DZL_SHORTCUT_PHASE_DISPATCH, widget, focus)) || + (match = dzl_shortcut_manager_run_phase (self, event, chord, DZL_SHORTCUT_PHASE_BUBBLE, widget, focus)) || + (match = dzl_shortcut_manager_run_global (self, event, chord, DZL_SHORTCUT_PHASE_BUBBLE, root, widget)) || + (match = dzl_shortcut_manager_run_fallbacks (self, widget, toplevel, chord))) + ret = GDK_EVENT_STOP; + + DZL_TRACE_MSG ("match = %d", match); + + /* No match, clear our current chord */ + if (match != DZL_SHORTCUT_MATCH_PARTIAL) + _dzl_shortcut_controller_clear (root); + + DZL_RETURN (ret); +} + +const gchar * +dzl_shortcut_manager_get_theme_name (DzlShortcutManager *self) +{ + DzlShortcutTheme *theme; + + g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), NULL); + + theme = dzl_shortcut_manager_get_theme (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (theme), NULL); + + return dzl_shortcut_theme_get_name (theme); +} + +void +dzl_shortcut_manager_set_theme_name (DzlShortcutManager *self, + const gchar *name) +{ + DzlShortcutManagerPrivate *priv; + + if (self == NULL) + self = dzl_shortcut_manager_get_default (); + + priv = dzl_shortcut_manager_get_instance_private (self); + + if (name == NULL) + name = "default"; + + for (guint i = 0; i < priv->themes->len; i++) + { + DzlShortcutTheme *theme = g_ptr_array_index (priv->themes, i); + const gchar *theme_name = dzl_shortcut_theme_get_name (theme); + + if (g_strcmp0 (name, theme_name) == 0) + { + dzl_shortcut_manager_set_theme (self, theme); + return; + } + } + + g_warning ("No such shortcut theme “%s”", name); +} + +static guint +dzl_shortcut_manager_get_n_items (GListModel *model) +{ + DzlShortcutManager *self = (DzlShortcutManager *)model; + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), 0); + + return priv->themes->len; +} + +static GType +dzl_shortcut_manager_get_item_type (GListModel *model) +{ + return DZL_TYPE_SHORTCUT_THEME; +} + +static gpointer +dzl_shortcut_manager_get_item (GListModel *model, + guint position) +{ + DzlShortcutManager *self = (DzlShortcutManager *)model; + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), NULL); + g_return_val_if_fail (position < priv->themes->len, NULL); + + return g_object_ref (g_ptr_array_index (priv->themes, position)); +} + +static void +list_model_iface_init (GListModelInterface *iface) +{ + iface->get_n_items = dzl_shortcut_manager_get_n_items; + iface->get_item_type = dzl_shortcut_manager_get_item_type; + iface->get_item = dzl_shortcut_manager_get_item; +} + +const gchar * +dzl_shortcut_manager_get_user_dir (DzlShortcutManager *self) +{ + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), NULL); + + if (priv->user_dir == NULL) + { + priv->user_dir = g_build_filename (g_get_user_data_dir (), + g_get_prgname (), + NULL); + } + + return priv->user_dir; +} + +void +dzl_shortcut_manager_set_user_dir (DzlShortcutManager *self, + const gchar *user_dir) +{ + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + g_return_if_fail (DZL_IS_SHORTCUT_MANAGER (self)); + + if (g_strcmp0 (user_dir, priv->user_dir) != 0) + { + g_free (priv->user_dir); + priv->user_dir = g_strdup (user_dir); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_USER_DIR]); + } +} + +void +dzl_shortcut_manager_remove_search_path (DzlShortcutManager *self, + const gchar *directory) +{ + DzlShortcutManagerPrivate *priv; + + g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self)); + g_return_if_fail (directory != NULL); + + if (self == NULL) + self = dzl_shortcut_manager_get_default (); + + priv = dzl_shortcut_manager_get_instance_private (self); + + for (GList *iter = priv->search_path.head; iter != NULL; iter = iter->next) + { + gchar *path = iter->data; + + if (g_strcmp0 (path, directory) == 0) + { + /* TODO: Remove any merged keybindings */ + + g_queue_delete_link (&priv->search_path, iter); + g_free (path); + + dzl_shortcut_manager_queue_reload (self); + + break; + } + } +} + +void +dzl_shortcut_manager_append_search_path (DzlShortcutManager *self, + const gchar *directory) +{ + DzlShortcutManagerPrivate *priv; + + g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self)); + g_return_if_fail (directory != NULL); + + if (self == NULL) + self = dzl_shortcut_manager_get_default (); + + priv = dzl_shortcut_manager_get_instance_private (self); + + g_queue_push_tail (&priv->search_path, g_strdup (directory)); + + dzl_shortcut_manager_queue_reload (self); +} + +void +dzl_shortcut_manager_prepend_search_path (DzlShortcutManager *self, + const gchar *directory) +{ + DzlShortcutManagerPrivate *priv; + + g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self)); + g_return_if_fail (directory != NULL); + + if (self == NULL) + self = dzl_shortcut_manager_get_default (); + + priv = dzl_shortcut_manager_get_instance_private (self); + + g_queue_push_head (&priv->search_path, g_strdup (directory)); + + dzl_shortcut_manager_queue_reload (self); +} + +/** + * dzl_shortcut_manager_get_search_path: + * @self: A #DzlShortcutManager + * + * This function will get the list of search path entries. These are used to + * load themes for the application. You should set this search path for + * themes before calling g_initable_init() on the search manager. + * + * Returns: (transfer none) (element-type utf8): A #GList containing each of + * the search path items used to load shortcut themes. + */ +const GList * +dzl_shortcut_manager_get_search_path (DzlShortcutManager *self) +{ + DzlShortcutManagerPrivate *priv; + + if (self == NULL) + self = dzl_shortcut_manager_get_default (); + + priv = dzl_shortcut_manager_get_instance_private (self); + + return priv->search_path.head; +} + +static GNode * +dzl_shortcut_manager_find_child (DzlShortcutManager *self, + GNode *parent, + DzlShortcutNodeType type, + const gchar *name) +{ + DzlShortcutNodeData *data; + + g_assert (DZL_IS_SHORTCUT_MANAGER (self)); + g_assert (parent != NULL); + g_assert (type != 0); + g_assert (name != NULL); + + for (GNode *iter = parent->children; iter != NULL; iter = iter->next) + { + data = iter->data; + + if (data->type == type && data->name == name) + return iter; + } + + return NULL; +} + +static GNode * +dzl_shortcut_manager_get_group (DzlShortcutManager *self, + const gchar *section, + const gchar *group) +{ + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + DzlShortcutNodeData *data; + GNode *parent; + GNode *node; + + g_assert (DZL_IS_SHORTCUT_MANAGER (self)); + g_assert (section != NULL); + g_assert (group != NULL); + + node = dzl_shortcut_manager_find_child (self, priv->root, DZL_SHORTCUT_NODE_SECTION, section); + + if (node == NULL) + { + data = g_slice_new0 (DzlShortcutNodeData); + data->type = DZL_SHORTCUT_NODE_SECTION; + data->name = g_intern_string (section); + data->title = g_intern_string (section); + data->subtitle = NULL; + + node = g_node_append_data (priv->root, data); + } + + parent = node; + + node = dzl_shortcut_manager_find_child (self, parent, DZL_SHORTCUT_NODE_GROUP, group); + + if (node == NULL) + { + data = g_slice_new0 (DzlShortcutNodeData); + data->type = DZL_SHORTCUT_NODE_GROUP; + data->name = g_intern_string (group); + data->title = g_intern_string (group); + data->subtitle = NULL; + + node = g_node_append_data (parent, data); + } + + g_assert (node != NULL); + + return node; +} + +void +dzl_shortcut_manager_add_action (DzlShortcutManager *self, + const gchar *detailed_action_name, + const gchar *section, + const gchar *group, + const gchar *title, + const gchar *subtitle) +{ + DzlShortcutNodeData *data; + GNode *parent; + + g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self)); + g_return_if_fail (detailed_action_name != NULL); + g_return_if_fail (title != NULL); + + if (self == NULL) + self = dzl_shortcut_manager_get_default (); + + section = g_intern_string (section); + group = g_intern_string (group); + title = g_intern_string (title); + subtitle = g_intern_string (subtitle); + + parent = dzl_shortcut_manager_get_group (self, section, group); + + g_assert (parent != NULL); + + data = g_slice_new0 (DzlShortcutNodeData); + data->type = DZL_SHORTCUT_NODE_ACTION; + data->name = g_intern_string (detailed_action_name); + data->title = title; + data->subtitle = subtitle; + + g_node_append_data (parent, data); + + g_signal_emit (self, signals [CHANGED], 0); +} + +void +dzl_shortcut_manager_add_command (DzlShortcutManager *self, + const gchar *command, + const gchar *section, + const gchar *group, + const gchar *title, + const gchar *subtitle) +{ + DzlShortcutNodeData *data; + GNode *parent; + + g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self)); + g_return_if_fail (command != NULL); + g_return_if_fail (title != NULL); + + if (self == NULL) + self = dzl_shortcut_manager_get_default (); + + section = g_intern_string (section); + group = g_intern_string (group); + title = g_intern_string (title); + subtitle = g_intern_string (subtitle); + + parent = dzl_shortcut_manager_get_group (self, section, group); + + g_assert (parent != NULL); + + data = g_slice_new0 (DzlShortcutNodeData); + data->type = DZL_SHORTCUT_NODE_COMMAND; + data->name = g_intern_string (command); + data->title = title; + data->subtitle = subtitle; + + g_node_append_data (parent, data); + + g_signal_emit (self, signals [CHANGED], 0); +} + +static DzlShortcutsShortcut * +create_shortcut (const DzlShortcutChord *chord, + const gchar *title, + const gchar *subtitle) +{ + g_autofree gchar *accel = dzl_shortcut_chord_to_string (chord); + + return g_object_new (DZL_TYPE_SHORTCUTS_SHORTCUT, + "accelerator", accel, + "subtitle", subtitle, + "title", title, + "visible", TRUE, + NULL); +} + +/** + * dzl_shortcut_manager_add_shortcuts_to_window: + * @self: A #DzlShortcutManager + * @window: A #DzlShortcutsWindow + * + * Adds shortcuts registered with the #DzlShortcutManager to the + * #DzlShortcutsWindow. + */ +void +dzl_shortcut_manager_add_shortcuts_to_window (DzlShortcutManager *self, + DzlShortcutsWindow *window) +{ + DzlShortcutManagerPrivate *priv; + DzlShortcutTheme *theme; + GNode *parent; + + g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self)); + g_return_if_fail (DZL_IS_SHORTCUTS_WINDOW (window)); + + if (self == NULL) + self = dzl_shortcut_manager_get_default (); + + priv = dzl_shortcut_manager_get_instance_private (self); + + theme = dzl_shortcut_manager_get_theme (self); + + /* + * The GNode tree is in four levels. priv->root is the root of the tree and + * contains no data items itself. It is just our stable root. The children + * of priv->root are our section nodes. Each section node has group nodes + * as children. Finally, the shortcut nodes are the leaves. + */ + + parent = priv->root; + + for (const GNode *sections = parent->children; sections != NULL; sections = sections->next) + { + DzlShortcutNodeData *section_data = sections->data; + DzlShortcutsSection *section; + + section = g_object_new (DZL_TYPE_SHORTCUTS_SECTION, + "title", section_data->title, + "section-name", section_data->title, + "visible", TRUE, + NULL); + + for (const GNode *groups = sections->children; groups != NULL; groups = groups->next) + { + DzlShortcutNodeData *group_data = groups->data; + DzlShortcutsGroup *group; + + group = g_object_new (DZL_TYPE_SHORTCUTS_GROUP, + "title", group_data->title, + "visible", TRUE, + NULL); + + for (const GNode *iter = groups->children; iter != NULL; iter = iter->next) + { + DzlShortcutNodeData *data = iter->data; + const DzlShortcutChord *chord = NULL; + DzlShortcutsShortcut *shortcut; + + if (data->type == DZL_SHORTCUT_NODE_ACTION) + chord = dzl_shortcut_theme_get_chord_for_action (theme, data->name); + else if (data->type == DZL_SHORTCUT_NODE_COMMAND) + chord = dzl_shortcut_theme_get_chord_for_command (theme, data->name); + + shortcut = create_shortcut (chord, data->title, data->subtitle); + gtk_container_add (GTK_CONTAINER (group), GTK_WIDGET (shortcut)); + } + + gtk_container_add (GTK_CONTAINER (section), GTK_WIDGET (group)); + } + + gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (section)); + } +} + +GNode * +_dzl_shortcut_manager_get_root (DzlShortcutManager *self) +{ + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), NULL); + + return priv->root; +} + +/** + * dzl_shortcut_manager_add_shortcut_entries: + * @self: (nullable): a #DzlShortcutManager or %NULL for the default + * @shortcuts: (array length=n_shortcuts): shortcuts to add + * @n_shortcuts: the number of entries in @shortcuts + * @translation_domain: (nullable): the gettext domain to use for translations + * + * This method will add @shortcuts to the #DzlShortcutManager. + * + * This provides a simple way for widgets to add their shortcuts to the manager + * so that they may be overriden by themes or the end user. + */ +void +dzl_shortcut_manager_add_shortcut_entries (DzlShortcutManager *self, + const DzlShortcutEntry *shortcuts, + guint n_shortcuts, + const gchar *translation_domain) +{ + DzlShortcutManagerPrivate *priv; + + g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self)); + g_return_if_fail (shortcuts != NULL || n_shortcuts == 0); + + if (self == NULL) + self = dzl_shortcut_manager_get_default (); + + priv = dzl_shortcut_manager_get_instance_private (self); + + /* Ignore duplicate calls with the same entries. This is out of convenience + * to allow registering shortcuts from instance init (and thusly after the + * GdkDisplay has been connected. + */ + if (g_hash_table_contains (priv->seen_entries, shortcuts)) + return; + + g_hash_table_insert (priv->seen_entries, (gpointer)shortcuts, NULL); + + for (guint i = 0; i < n_shortcuts; i++) + { + const DzlShortcutEntry *entry = &shortcuts[i]; + + if (entry->command == NULL) + { + g_warning ("Shortcut entry missing command id"); + continue; + } + + if (entry->default_accel != NULL) + dzl_shortcut_theme_set_accel_for_command (priv->internal_theme, + entry->command, + entry->default_accel, + entry->phase); + + dzl_shortcut_manager_add_command (self, + entry->command, + g_dgettext (translation_domain, entry->section), + g_dgettext (translation_domain, entry->group), + g_dgettext (translation_domain, entry->title), + g_dgettext (translation_domain, entry->subtitle)); + } +} + +/** + * dzl_shortcut_manager_get_theme_by_name: + * @self: a #DzlShortcutManager + * @theme_name: (nullable): the name of a theme or %NULL of the internal theme + * + * Locates a theme by the name of the theme. + * + * If @theme_name is %NULL, then the internal theme is used. You probably dont + * need to use that as it is used by various controllers to hook up their + * default actions. + * + * Returns: (transfer none) (nullable): A #DzlShortcutTheme or %NULL. + */ +DzlShortcutTheme * +dzl_shortcut_manager_get_theme_by_name (DzlShortcutManager *self, + const gchar *theme_name) +{ + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), NULL); + + if (theme_name == NULL || g_strcmp0 (theme_name, "internal") == 0) + return priv->internal_theme; + + for (guint i = 0; i < priv->themes->len; i++) + { + DzlShortcutTheme *theme = g_ptr_array_index (priv->themes, i); + + g_assert (DZL_IS_SHORTCUT_THEME (theme)); + + if (g_strcmp0 (theme_name, dzl_shortcut_theme_get_name (theme)) == 0) + return theme; + } + + return NULL; +} + +DzlShortcutTheme * +_dzl_shortcut_manager_get_internal_theme (DzlShortcutManager *self) +{ + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), NULL); + + return priv->internal_theme; +} + +static void +dzl_shortcut_manager_merge (DzlShortcutManager *self, + DzlShortcutTheme *theme) +{ + DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self); + g_autoptr(DzlShortcutTheme) alloc_layer = NULL; + DzlShortcutTheme *base_layer; + const gchar *name; + + DZL_ENTRY; + + g_return_if_fail (DZL_IS_SHORTCUT_MANAGER (self)); + g_return_if_fail (DZL_IS_SHORTCUT_THEME (theme)); + + /* + * One thing we are trying to avoid here is having separate code paths for + * adding the "first theme modification" from merging additional layers from + * plugins and the like. Having the same merge path in all situations + * hopefully will help us avoid some bugs. + */ + + name = dzl_shortcut_theme_get_name (theme); + + if (dzl_str_empty0 (name)) + { + g_warning ("Attempt to merge theme with empty name"); + DZL_EXIT; + } + + base_layer = dzl_shortcut_manager_get_theme_by_name (self, name); + + if (base_layer == NULL) + { + const gchar *parent_name; + const gchar *title; + const gchar *subtitle; + + parent_name = dzl_shortcut_theme_get_parent_name (theme); + title = dzl_shortcut_theme_get_title (theme); + subtitle = dzl_shortcut_theme_get_subtitle (theme); + + alloc_layer = g_object_new (DZL_TYPE_SHORTCUT_THEME, + "name", name, + "parent-name", parent_name, + "subtitle", subtitle, + "title", title, + NULL); + + base_layer = alloc_layer; + + /* + * Now notify the GListModel consumers that our internal theme list + * has changed to include the newly created base layer. + */ + g_ptr_array_add (priv->themes, g_object_ref (alloc_layer)); + _dzl_shortcut_theme_set_manager (alloc_layer, self); + g_list_model_items_changed (G_LIST_MODEL (self), priv->themes->len - 1, 0, 1); + } + + /* + * Okay, now we need to go through all the custom contexts, and global + * shortcuts in the theme and merge them into the base_layer. However, we + * will defer that work to the DzlShortcutTheme module so it has access to + * the internal structures. + */ + _dzl_shortcut_theme_merge (base_layer, theme); + + DZL_EXIT; +} diff --git a/src/shortcuts/dzl-shortcut-manager.h b/src/shortcuts/dzl-shortcut-manager.h new file mode 100644 index 0000000..2863b3c --- /dev/null +++ b/src/shortcuts/dzl-shortcut-manager.h @@ -0,0 +1,139 @@ +/* dzl-shortcut-manager.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 2 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 . + */ + +#ifndef DZL_SHORTCUT_MANAGER_H +#define DZL_SHORTCUT_MANAGER_H + +#include + +#include "dzl-version-macros.h" + +#include "dzl-shortcut-phase.h" +#include "dzl-shortcut-theme.h" +#include "dzl-shortcuts-window.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUT_MANAGER (dzl_shortcut_manager_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlShortcutManager, dzl_shortcut_manager, DZL, SHORTCUT_MANAGER, GObject) + +/** + * DzlShortcutEntry: + * @command: the command identifier + * @phase: the phase for activation, or 0 for the default + * @default_accel: the default accelerator for the command, if any + * @section: the section for the shortcuts window + * @group: the group for the shortcuts window + * @title: the title for the shortcuts window + * @subtitle: the subtitle for the shortcuts window, if any + * + * The #DzlShortcutEntry structure can be used to bulk register shortcuts + * for a particular widget. It can also do the necessary hooks of registering + * commands that can be changed using the keytheme components. + */ +typedef struct +{ + const gchar *command; + DzlShortcutPhase phase; + const gchar *default_accel; + const gchar *section; + const gchar *group; + const gchar *title; + const gchar *subtitle; +} DzlShortcutEntry; + +struct _DzlShortcutManagerClass +{ + GObjectClass parent_instance; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +DzlShortcutManager *dzl_shortcut_manager_get_default (void); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_manager_queue_reload (DzlShortcutManager *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_manager_reload (DzlShortcutManager *self, + GCancellable *cancellable); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_manager_append_search_path (DzlShortcutManager *self, + const gchar *directory); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_manager_prepend_search_path (DzlShortcutManager *self, + const gchar *directory); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_manager_remove_search_path (DzlShortcutManager *self, + const gchar *directory); +DZL_AVAILABLE_IN_ALL +DzlShortcutTheme *dzl_shortcut_manager_get_theme (DzlShortcutManager *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_manager_set_theme (DzlShortcutManager *self, + DzlShortcutTheme *theme); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_shortcut_manager_get_theme_name (DzlShortcutManager *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_manager_set_theme_name (DzlShortcutManager *self, + const gchar *theme_name); +DZL_AVAILABLE_IN_ALL +DzlShortcutTheme *dzl_shortcut_manager_get_theme_by_name (DzlShortcutManager *self, + const gchar *theme_name); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_manager_handle_event (DzlShortcutManager *self, + const GdkEventKey *event, + GtkWidget *toplevel); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_shortcut_manager_get_user_dir (DzlShortcutManager *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_manager_set_user_dir (DzlShortcutManager *self, + const gchar *user_dir); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_manager_add_action (DzlShortcutManager *self, + const gchar *detailed_action_name, + const gchar *section, + const gchar *group, + const gchar *title, + const gchar *subtitle); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_manager_add_command (DzlShortcutManager *self, + const gchar *command, + const gchar *section, + const gchar *group, + const gchar *title, + const gchar *subtitle); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_manager_add_shortcut_entries (DzlShortcutManager *self, + const DzlShortcutEntry *shortcuts, + guint n_shortcuts, + const gchar *translation_domain); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_manager_add_shortcuts_to_window (DzlShortcutManager *self, + DzlShortcutsWindow *window); + +G_END_DECLS + +#endif /* DZL_SHORTCUT_MANAGER_H */ diff --git a/src/shortcuts/dzl-shortcut-model.c b/src/shortcuts/dzl-shortcut-model.c new file mode 100644 index 0000000..d3db622 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-model.c @@ -0,0 +1,333 @@ +/* dzl-shortcut-model.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-model" + +#include "config.h" + +#include "shortcuts/dzl-shortcut-model.h" +#include "shortcuts/dzl-shortcut-private.h" + +struct _DzlShortcutModel +{ + GtkTreeStore parent_instance; + DzlShortcutManager *manager; + DzlShortcutTheme *theme; +}; + +G_DEFINE_TYPE (DzlShortcutModel, dzl_shortcut_model, GTK_TYPE_TREE_STORE) + +enum { + PROP_0, + PROP_MANAGER, + PROP_THEME, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +void +dzl_shortcut_model_rebuild (DzlShortcutModel *self) +{ + g_assert (DZL_IS_SHORTCUT_MODEL (self)); + + gtk_tree_store_clear (GTK_TREE_STORE (self)); + + if (self->manager != NULL && self->theme != NULL) + { + GNode *root; + + root = _dzl_shortcut_manager_get_root (self->manager); + + for (const GNode *iter = root->children; iter != NULL; iter = iter->next) + { + for (const GNode *groups = iter->children; groups != NULL; groups = groups->next) + { + DzlShortcutNodeData *group = groups->data; + GtkTreeIter p; + + gtk_tree_store_append (GTK_TREE_STORE (self), &p, NULL); + gtk_tree_store_set (GTK_TREE_STORE (self), &p, + DZL_SHORTCUT_MODEL_COLUMN_TITLE, group->title, + -1); + + for (const GNode *sc = groups->children; sc != NULL; sc = sc->next) + { + DzlShortcutNodeData *shortcut = sc->data; + const DzlShortcutChord *chord = NULL; + g_autofree gchar *accel = NULL; + g_autofree gchar *down = NULL; + GtkTreeIter p2; + + if (shortcut->type == DZL_SHORTCUT_NODE_ACTION) + chord = dzl_shortcut_theme_get_chord_for_action (self->theme, shortcut->name); + else if (shortcut->type == DZL_SHORTCUT_NODE_COMMAND) + chord = dzl_shortcut_theme_get_chord_for_command (self->theme, shortcut->name); + + accel = dzl_shortcut_chord_get_label (chord); + down = g_utf8_casefold (shortcut->title, -1); + + gtk_tree_store_append (GTK_TREE_STORE (self), &p2, &p); + gtk_tree_store_set (GTK_TREE_STORE (self), &p2, + DZL_SHORTCUT_MODEL_COLUMN_TYPE, shortcut->type, + DZL_SHORTCUT_MODEL_COLUMN_ID, shortcut->name, + DZL_SHORTCUT_MODEL_COLUMN_TITLE, shortcut->title, + DZL_SHORTCUT_MODEL_COLUMN_ACCEL, accel, + DZL_SHORTCUT_MODEL_COLUMN_KEYWORDS, down, + DZL_SHORTCUT_MODEL_COLUMN_CHORD, chord, + -1); + } + } + } + } +} + +static void +dzl_shortcut_model_constructed (GObject *object) +{ + DzlShortcutModel *self = (DzlShortcutModel *)object; + + g_assert (DZL_IS_SHORTCUT_MODEL (self)); + + G_OBJECT_CLASS (dzl_shortcut_model_parent_class)->constructed (object); + + dzl_shortcut_model_rebuild (self); +} + +static void +dzl_shortcut_model_finalize (GObject *object) +{ + DzlShortcutModel *self = (DzlShortcutModel *)object; + + g_clear_object (&self->manager); + g_clear_object (&self->theme); + + G_OBJECT_CLASS (dzl_shortcut_model_parent_class)->finalize (object); +} + +static void +dzl_shortcut_model_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutModel *self = DZL_SHORTCUT_MODEL (object); + + switch (prop_id) + { + case PROP_MANAGER: + g_value_set_object (value, dzl_shortcut_model_get_manager (self)); + break; + + case PROP_THEME: + g_value_set_object (value, dzl_shortcut_model_get_theme (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_model_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutModel *self = DZL_SHORTCUT_MODEL (object); + + switch (prop_id) + { + case PROP_MANAGER: + dzl_shortcut_model_set_manager (self, g_value_get_object (value)); + break; + + case PROP_THEME: + dzl_shortcut_model_set_theme (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_model_class_init (DzlShortcutModelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = dzl_shortcut_model_constructed; + object_class->finalize = dzl_shortcut_model_finalize; + object_class->get_property = dzl_shortcut_model_get_property; + object_class->set_property = dzl_shortcut_model_set_property; + + properties [PROP_MANAGER] = + g_param_spec_object ("manager", + "Manager", + "Manager", + DZL_TYPE_SHORTCUT_MANAGER, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_THEME] = + g_param_spec_object ("theme", + "Theme", + "Theme", + DZL_TYPE_SHORTCUT_THEME, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_shortcut_model_init (DzlShortcutModel *self) +{ + GType element_types[] = { + G_TYPE_INT, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_STRING, + DZL_TYPE_SHORTCUT_CHORD, + }; + + G_STATIC_ASSERT (G_N_ELEMENTS (element_types) == DZL_SHORTCUT_MODEL_N_COLUMNS); + + self->manager = g_object_ref (dzl_shortcut_manager_get_default ()); + + gtk_tree_store_set_column_types (GTK_TREE_STORE (self), + G_N_ELEMENTS (element_types), + element_types); +} + +/** + * dzl_shortcut_model_new: + * + * Returns: (transfer full): A #GtkTreeModel + */ +GtkTreeModel * +dzl_shortcut_model_new (void) +{ + return g_object_new (DZL_TYPE_SHORTCUT_MODEL, NULL); +} + +/** + * dzl_shortcut_model_get_manager: + * @self: a #DzlShortcutModel + * + * Gets the manager to be edited. + * + * Returns: (transfer none): A #DzlShortcutManager + */ +DzlShortcutManager * +dzl_shortcut_model_get_manager (DzlShortcutModel *self) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_MODEL (self), NULL); + + return self->manager; +} + +void +dzl_shortcut_model_set_manager (DzlShortcutModel *self, + DzlShortcutManager *manager) +{ + g_return_if_fail (DZL_IS_SHORTCUT_MODEL (self)); + g_return_if_fail (!manager || DZL_IS_SHORTCUT_MANAGER (manager)); + + if (g_set_object (&self->manager, manager)) + { + dzl_shortcut_model_rebuild (self); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MANAGER]); + } +} + +/** + * dzl_shortcut_model_get_theme: + * @self: a #DzlShortcutModel + * + * Get the theme to be edited. + * + * Returns: (transfer none): A #DzlShortcutTheme + */ +DzlShortcutTheme * +dzl_shortcut_model_get_theme (DzlShortcutModel *self) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_MODEL (self), NULL); + + return self->theme; +} + +void +dzl_shortcut_model_set_theme (DzlShortcutModel *self, + DzlShortcutTheme *theme) +{ + g_return_if_fail (DZL_IS_SHORTCUT_MODEL (self)); + g_return_if_fail (!theme || DZL_IS_SHORTCUT_THEME (theme)); + + if (g_set_object (&self->theme, theme)) + { + dzl_shortcut_model_rebuild (self); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME]); + } +} + +static void +dzl_shortcut_model_apply (DzlShortcutModel *self, + GtkTreeIter *iter) +{ + g_autoptr(DzlShortcutChord) chord = NULL; + g_autofree gchar *id = NULL; + gint type = 0; + + g_assert (DZL_IS_SHORTCUT_MODEL (self)); + g_assert (DZL_IS_SHORTCUT_THEME (self->theme)); + g_assert (iter != NULL); + + gtk_tree_model_get (GTK_TREE_MODEL (self), iter, + DZL_SHORTCUT_MODEL_COLUMN_TYPE, &type, + DZL_SHORTCUT_MODEL_COLUMN_ID, &id, + DZL_SHORTCUT_MODEL_COLUMN_CHORD, &chord, + -1); + + if (type == DZL_SHORTCUT_NODE_ACTION) + dzl_shortcut_theme_set_chord_for_action (self->theme, id, chord, 0); + else if (type == DZL_SHORTCUT_NODE_COMMAND) + dzl_shortcut_theme_set_chord_for_command (self->theme, id, chord, 0); + else + g_warning ("Unknown type: %d", type); +} + +void +dzl_shortcut_model_set_chord (DzlShortcutModel *self, + GtkTreeIter *iter, + const DzlShortcutChord *chord) +{ + g_autofree gchar *accel = NULL; + + g_return_if_fail (DZL_IS_SHORTCUT_MODEL (self)); + g_return_if_fail (iter != NULL); + g_return_if_fail (gtk_tree_store_iter_is_valid (GTK_TREE_STORE (self), iter)); + + accel = dzl_shortcut_chord_get_label (chord); + + gtk_tree_store_set (GTK_TREE_STORE (self), iter, + DZL_SHORTCUT_MODEL_COLUMN_ACCEL, accel, + DZL_SHORTCUT_MODEL_COLUMN_CHORD, chord, + -1); + + dzl_shortcut_model_apply (self, iter); +} diff --git a/src/shortcuts/dzl-shortcut-model.h b/src/shortcuts/dzl-shortcut-model.h new file mode 100644 index 0000000..524b871 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-model.h @@ -0,0 +1,58 @@ +/* dzl-shortcut-model.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#ifndef DZL_SHORTCUT_MODEL_H +#define DZL_SHORTCUT_MODEL_H + +#include + +#include "dzl-version-macros.h" + +#include "dzl-shortcut-chord.h" +#include "dzl-shortcut-manager.h" +#include "dzl-shortcut-theme.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUT_MODEL (dzl_shortcut_model_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlShortcutModel, dzl_shortcut_model, DZL, SHORTCUT_MODEL, GtkTreeStore) + +DZL_AVAILABLE_IN_ALL +GtkTreeModel *dzl_shortcut_model_new (void); +DZL_AVAILABLE_IN_ALL +DzlShortcutManager *dzl_shortcut_model_get_manager (DzlShortcutModel *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_model_set_manager (DzlShortcutModel *self, + DzlShortcutManager *manager); +DZL_AVAILABLE_IN_ALL +DzlShortcutTheme *dzl_shortcut_model_get_theme (DzlShortcutModel *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_model_set_theme (DzlShortcutModel *self, + DzlShortcutTheme *theme); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_model_set_chord (DzlShortcutModel *self, + GtkTreeIter *iter, + const DzlShortcutChord *chord); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_model_rebuild (DzlShortcutModel *self); + +G_END_DECLS + +#endif /* DZL_SHORTCUT_MODEL_H */ diff --git a/src/shortcuts/dzl-shortcut-phase.c b/src/shortcuts/dzl-shortcut-phase.c new file mode 100644 index 0000000..aadad65 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-phase.c @@ -0,0 +1,44 @@ +/* dzl-shortcut-phase.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-phase" + +#include "config.h" + +#include "shortcuts/dzl-shortcut-phase.h" + +GType +dzl_shortcut_phase_get_type (void) +{ + static GType type_id; + + if (g_once_init_enter (&type_id)) + { + static const GFlagsValue values[] = { + { DZL_SHORTCUT_PHASE_DISPATCH, "DZL_SHORTCUT_PHASE_DISPATCH", "dispatch" }, + { DZL_SHORTCUT_PHASE_CAPTURE, "DZL_SHORTCUT_PHASE_CAPTURE", "capture" }, + { DZL_SHORTCUT_PHASE_BUBBLE, "DZL_SHORTCUT_PHASE_BUBBLE", "bubble" }, + { DZL_SHORTCUT_PHASE_GLOBAL, "DZL_SHORTCUT_PHASE_GLOBAL", "global" }, + { 0 } + }; + GType _type_id = g_flags_register_static ("DzlShortcutPhase", values); + g_once_init_leave (&type_id, _type_id); + } + + return type_id; +} diff --git a/src/shortcuts/dzl-shortcut-phase.h b/src/shortcuts/dzl-shortcut-phase.h new file mode 100644 index 0000000..d02784f --- /dev/null +++ b/src/shortcuts/dzl-shortcut-phase.h @@ -0,0 +1,56 @@ +/* dzl-shortcut-phase.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_SHORTCUT_PHASE_H +#define DZL_SHORTCUT_PHASE_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUT_PHASE (dzl_shortcut_phase_get_type()) + +/** + * DzlShortcutPhase: + * @DZL_SHORTCUT_PHASE_CAPTURE: Indicates the capture phase of the shortcut + * activation. This allows parent widgets to intercept the keybinding before + * it is dispatched to the target #GdkWindow. + * @DZL_SHORTCUT_DISPATCH: Indicates the typical dispatch phase of the shortcut + * to the widget of the target #GdkWindow. + * @DZL_SHORTCUT_BUBBLE: The final phase of event delivery. The event is + * delivered to each widget as it progresses from the target window widget + * up to the toplevel. + * @DZL_SHORTCUT_GLOBAL: The shortcut can be activated from anywhere in the + * widget hierarchy, even outside the direct chain of focus. + */ +typedef enum +{ + DZL_SHORTCUT_PHASE_DISPATCH = 0, + DZL_SHORTCUT_PHASE_CAPTURE = 1 << 0, + DZL_SHORTCUT_PHASE_BUBBLE = 1 << 1, + DZL_SHORTCUT_PHASE_GLOBAL = 1 << 2, +} DzlShortcutPhase; + +DZL_AVAILABLE_IN_ALL +GType dzl_shortcut_phase_get_type (void); + +G_END_DECLS + +#endif /* DZL_SHORTCUT_PHASE_H */ diff --git a/src/shortcuts/dzl-shortcut-private.h b/src/shortcuts/dzl-shortcut-private.h new file mode 100644 index 0000000..4afcad1 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-private.h @@ -0,0 +1,151 @@ +/* dzl-shortcut-private.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#ifndef DZL_SHORTCUT_THEME_PRIVATE_H +#define DZL_SHORTCUT_THEME_PRIVATE_H + +#include "shortcuts/dzl-shortcut-chord.h" +#include "shortcuts/dzl-shortcut-closure-chain.h" +#include "shortcuts/dzl-shortcut-controller.h" +#include "shortcuts/dzl-shortcut-simple-label.h" +#include "shortcuts/dzl-shortcut-manager.h" +#include "shortcuts/dzl-shortcut-theme.h" + +G_BEGIN_DECLS + +typedef struct +{ + DzlShortcutChordTable *table; + guint position; +} DzlShortcutChordTableIter; + +typedef enum +{ + DZL_SHORTCUT_NODE_SECTION = 1, + DZL_SHORTCUT_NODE_GROUP, + DZL_SHORTCUT_NODE_ACTION, + DZL_SHORTCUT_NODE_COMMAND, +} DzlShortcutNodeType; + +typedef struct +{ + DzlShortcutNodeType type; + const gchar *name; + const gchar *title; + const gchar *subtitle; +} DzlShortcutNodeData; + +typedef enum +{ + DZL_SHORTCUT_MODEL_COLUMN_TYPE, + DZL_SHORTCUT_MODEL_COLUMN_ID, + DZL_SHORTCUT_MODEL_COLUMN_TITLE, + DZL_SHORTCUT_MODEL_COLUMN_ACCEL, + DZL_SHORTCUT_MODEL_COLUMN_KEYWORDS, + DZL_SHORTCUT_MODEL_COLUMN_CHORD, + DZL_SHORTCUT_MODEL_N_COLUMNS +} DzlShortcutModelColumn; + +typedef enum +{ + DZL_SHORTCUT_CLOSURE_ACTION = 1, + DZL_SHORTCUT_CLOSURE_CALLBACK, + DZL_SHORTCUT_CLOSURE_COMMAND, + DZL_SHORTCUT_CLOSURE_SIGNAL, + DZL_SHORTCUT_CLOSURE_LAST +} DzlShortcutClosureType; + +struct _DzlShortcutClosureChain +{ + GSList node; + + DzlShortcutClosureType type : 3; + DzlShortcutPhase phase : 3; + guint executing : 1; + + union { + struct { + const gchar *group; + const gchar *name; + GVariant *params; + } action; + struct { + const gchar *name; + } command; + struct { + GQuark detail; + const gchar *name; + GArray *params; + } signal; + struct { + GtkCallback callback; + gpointer user_data; + GDestroyNotify notify; + } callback; + }; +}; + +DzlShortcutMatch _dzl_shortcut_controller_handle (DzlShortcutController *self, + const GdkEventKey *event, + const DzlShortcutChord *chord, + DzlShortcutPhase phase, + GtkWidget *widget); +DzlShortcutChord *_dzl_shortcut_controller_push (DzlShortcutController *self, + const GdkEventKey *event); +void _dzl_shortcut_controller_clear (DzlShortcutController *self); +GNode *_dzl_shortcut_manager_get_root (DzlShortcutManager *self); +DzlShortcutTheme *_dzl_shortcut_manager_get_internal_theme (DzlShortcutManager *self); +void _dzl_shortcut_simple_label_set_size_group (DzlShortcutSimpleLabel *self, + GtkSizeGroup *size_group); +void _dzl_shortcut_theme_attach (DzlShortcutTheme *self); +void _dzl_shortcut_theme_detach (DzlShortcutTheme *self); +GtkTreeModel *_dzl_shortcut_theme_create_model (DzlShortcutTheme *self); +GHashTable *_dzl_shortcut_theme_get_contexts (DzlShortcutTheme *self); +DzlShortcutContext *_dzl_shortcut_theme_try_find_context_by_name (DzlShortcutTheme *self, + const gchar *name); +DzlShortcutContext *_dzl_shortcut_theme_find_default_context_with_phase + (DzlShortcutTheme *self, + GtkWidget *widget, + DzlShortcutPhase phase); +void _dzl_shortcut_theme_set_manager (DzlShortcutTheme *self, + DzlShortcutManager *manager); +void _dzl_shortcut_theme_set_name (DzlShortcutTheme *self, + const gchar *name); +const gchar *_dzl_shortcut_theme_lookup_action (DzlShortcutTheme *self, + const DzlShortcutChord *chord); +void _dzl_shortcut_theme_merge (DzlShortcutTheme *self, + DzlShortcutTheme *layer); +DzlShortcutMatch _dzl_shortcut_theme_match (DzlShortcutTheme *self, + DzlShortcutPhase phase, + const DzlShortcutChord *chord, + DzlShortcutClosureChain **chain); +gboolean _dzl_shortcut_context_contains (DzlShortcutContext *self, + const DzlShortcutChord *chord); +DzlShortcutChordTable *_dzl_shortcut_context_get_table (DzlShortcutContext *self); +void _dzl_shortcut_context_merge (DzlShortcutContext *self, + DzlShortcutContext *layer); +void _dzl_shortcut_chord_table_iter_init (DzlShortcutChordTableIter *iter, + DzlShortcutChordTable *table); +gboolean _dzl_shortcut_chord_table_iter_next (DzlShortcutChordTableIter *iter, + const DzlShortcutChord **chord, + gpointer *value); +void _dzl_shortcut_chord_table_iter_steal (DzlShortcutChordTableIter *iter); + +G_END_DECLS + +#endif /* DZL_SHORTCUT_THEME_PRIVATE_H */ diff --git a/src/shortcuts/dzl-shortcut-simple-label.c b/src/shortcuts/dzl-shortcut-simple-label.c new file mode 100644 index 0000000..5fae8ff --- /dev/null +++ b/src/shortcuts/dzl-shortcut-simple-label.c @@ -0,0 +1,308 @@ +/* dzl-shortcut-simple-label.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-simple-label" + +#include "config.h" + +#include "shortcuts/dzl-shortcut-chord.h" +#include "shortcuts/dzl-shortcut-simple-label.h" +#include "util/dzl-gtk.h" + +struct _DzlShortcutSimpleLabel +{ + GtkBox parent_instance; + + GtkLabel *accel_label; + GtkLabel *title; + + const gchar *accel; + const gchar *action; + const gchar *command; +}; + +enum { + PROP_0, + PROP_ACCEL, + PROP_ACTION, + PROP_COMMAND, + PROP_SHOW_ACCEL, + PROP_TITLE, + N_PROPS +}; + +G_DEFINE_TYPE (DzlShortcutSimpleLabel, dzl_shortcut_simple_label, GTK_TYPE_BOX) + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_shortcut_simple_label_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutSimpleLabel *self = DZL_SHORTCUT_SIMPLE_LABEL (object); + + switch (prop_id) + { + case PROP_ACCEL: + g_value_set_static_string (value, dzl_shortcut_simple_label_get_accel (self)); + break; + + case PROP_ACTION: + g_value_set_static_string (value, dzl_shortcut_simple_label_get_action (self)); + break; + + case PROP_COMMAND: + g_value_set_static_string (value, dzl_shortcut_simple_label_get_command (self)); + break; + + case PROP_SHOW_ACCEL: + g_object_get_property (G_OBJECT (self->accel_label), "visible", value); + break; + + case PROP_TITLE: + g_value_set_string (value, dzl_shortcut_simple_label_get_title (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_simple_label_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutSimpleLabel *self = DZL_SHORTCUT_SIMPLE_LABEL (object); + + switch (prop_id) + { + case PROP_ACCEL: + dzl_shortcut_simple_label_set_accel (self, g_value_get_string (value)); + break; + + case PROP_ACTION: + dzl_shortcut_simple_label_set_action (self, g_value_get_string (value)); + break; + + case PROP_COMMAND: + dzl_shortcut_simple_label_set_command (self, g_value_get_string (value)); + break; + + case PROP_SHOW_ACCEL: + g_object_set_property (G_OBJECT (self->accel_label), "visible", value); + break; + + case PROP_TITLE: + dzl_shortcut_simple_label_set_title (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_simple_label_class_init (DzlShortcutSimpleLabelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = dzl_shortcut_simple_label_get_property; + object_class->set_property = dzl_shortcut_simple_label_set_property; + + properties [PROP_ACTION] = + g_param_spec_string ("action", + "Action", + "Action", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_ACCEL] = + g_param_spec_string ("accel", + "Accel", + "The accel label to override the discovered accel", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_COMMAND] = + g_param_spec_string ("command", + "Command", + "Command", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_ACCEL] = + g_param_spec_boolean ("show-accel", NULL, NULL, + TRUE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "Title", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_shortcut_simple_label_init (DzlShortcutSimpleLabel *self) +{ + self->title = g_object_new (GTK_TYPE_LABEL, + "visible", TRUE, + "use-underline", TRUE, + "xalign", 0.0f, + NULL); + dzl_gtk_widget_add_style_class (GTK_WIDGET (self->title), "title"); + gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (self->title), + "fill", TRUE, + "pack-type", GTK_PACK_START, + NULL); + + self->accel_label = g_object_new (GTK_TYPE_LABEL, + "hexpand", TRUE, + "halign", GTK_ALIGN_START, + "margin-start", 12, + "visible", TRUE, + "xalign", 0.0f, + NULL); + dzl_gtk_widget_add_style_class (GTK_WIDGET (self->accel_label), "dim-label"); + gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (self->accel_label), + "fill", TRUE, + "pack-type", GTK_PACK_END, + NULL); +} + +GtkWidget * +dzl_shortcut_simple_label_new (void) +{ + return g_object_new (DZL_TYPE_SHORTCUT_SIMPLE_LABEL, NULL); +} + +const gchar * +dzl_shortcut_simple_label_get_accel (DzlShortcutSimpleLabel *self) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_SIMPLE_LABEL (self), NULL); + + return self->accel; +} + +const gchar * +dzl_shortcut_simple_label_get_action (DzlShortcutSimpleLabel *self) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_SIMPLE_LABEL (self), NULL); + + return self->action; +} + +const gchar * +dzl_shortcut_simple_label_get_command (DzlShortcutSimpleLabel *self) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_SIMPLE_LABEL (self), NULL); + + return self->command; +} + +const gchar * +dzl_shortcut_simple_label_get_title (DzlShortcutSimpleLabel *self) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_SIMPLE_LABEL (self), NULL); + + return gtk_label_get_label (self->title); +} + +void +dzl_shortcut_simple_label_set_accel (DzlShortcutSimpleLabel *self, + const gchar *accel) +{ + g_return_if_fail (DZL_IS_SHORTCUT_SIMPLE_LABEL (self)); + + accel = g_intern_string (accel); + + if (accel != self->accel) + { + g_autofree gchar *label = NULL; + + self->accel = accel; + + if (accel != NULL) + { + g_autoptr(DzlShortcutChord) chord = NULL; + + chord = dzl_shortcut_chord_new_from_string (accel); + label = dzl_shortcut_chord_get_label (chord); + } + + gtk_label_set_label (self->accel_label, label); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACCEL]); + } +} + +void +dzl_shortcut_simple_label_set_action (DzlShortcutSimpleLabel *self, + const gchar *action) +{ + g_return_if_fail (DZL_IS_SHORTCUT_SIMPLE_LABEL (self)); + + action = g_intern_string (action); + + if (action != self->action) + { + self->action = action; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTION]); + } +} + +void +dzl_shortcut_simple_label_set_command (DzlShortcutSimpleLabel *self, + const gchar *command) +{ + g_return_if_fail (DZL_IS_SHORTCUT_SIMPLE_LABEL (self)); + + command = g_intern_string (command); + + if (command != self->command) + { + self->command = command; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMMAND]); + } +} + +void +dzl_shortcut_simple_label_set_title (DzlShortcutSimpleLabel *self, + const gchar *title) +{ + g_return_if_fail (DZL_IS_SHORTCUT_SIMPLE_LABEL (self)); + + gtk_label_set_label (self->title, title); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]); +} + +void +_dzl_shortcut_simple_label_set_size_group(DzlShortcutSimpleLabel *self, + GtkSizeGroup *size_group) +{ + g_return_if_fail (DZL_IS_SHORTCUT_SIMPLE_LABEL (self)); + + if (size_group != NULL) + gtk_size_group_add_widget (size_group, GTK_WIDGET (self->title)); +} diff --git a/src/shortcuts/dzl-shortcut-simple-label.h b/src/shortcuts/dzl-shortcut-simple-label.h new file mode 100644 index 0000000..d61d140 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-simple-label.h @@ -0,0 +1,58 @@ +/* dzl-shortcut-simple-label.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_SHORTCUT_SIMPLE_LABEL_H +#define DZL_SHORTCUT_SIMPLE_LABEL_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUT_SIMPLE_LABEL (dzl_shortcut_simple_label_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlShortcutSimpleLabel, dzl_shortcut_simple_label, DZL, SHORTCUT_SIMPLE_LABEL, GtkBox) + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_shortcut_simple_label_new (void); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_shortcut_simple_label_get_accel (DzlShortcutSimpleLabel *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_simple_label_set_accel (DzlShortcutSimpleLabel *self, + const gchar *accel); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_shortcut_simple_label_get_action (DzlShortcutSimpleLabel *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_simple_label_set_action (DzlShortcutSimpleLabel *self, + const gchar *action); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_shortcut_simple_label_get_command (DzlShortcutSimpleLabel *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_simple_label_set_command (DzlShortcutSimpleLabel *self, + const gchar *command); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_shortcut_simple_label_get_title (DzlShortcutSimpleLabel *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_simple_label_set_title (DzlShortcutSimpleLabel *self, + const gchar *title); + +G_END_DECLS + +#endif /* DZL_SHORTCUT_SIMPLE_LABEL_H */ diff --git a/src/shortcuts/dzl-shortcut-theme-editor.c b/src/shortcuts/dzl-shortcut-theme-editor.c new file mode 100644 index 0000000..b390dda --- /dev/null +++ b/src/shortcuts/dzl-shortcut-theme-editor.c @@ -0,0 +1,480 @@ +/* dzl-shortcut-theme-editor.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-theme-editor" + +#include "config.h" + +#include + +#include "shortcuts/dzl-shortcut-accel-dialog.h" +#include "shortcuts/dzl-shortcut-model.h" +#include "shortcuts/dzl-shortcut-private.h" +#include "shortcuts/dzl-shortcut-theme-editor.h" +#include "util/dzl-util-private.h" + +typedef struct +{ + GtkTreeView *tree_view; + GtkSearchEntry *filter_entry; + GtkTreeViewColumn *shortcut_column; + GtkCellRendererText *shortcut_cell; + GtkTreeViewColumn *title_column; + GtkCellRendererText *title_cell; + + DzlShortcutTheme *theme; + GtkTreeModel *model; + GtkTreePath *selected; + PangoAttrList *attrs; +} DzlShortcutThemeEditorPrivate; + +enum { + PROP_0, + PROP_THEME, + N_PROPS +}; + +enum { + CHANGED, + N_SIGNALS +}; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlShortcutThemeEditor, dzl_shortcut_theme_editor, GTK_TYPE_BIN) + +static GParamSpec *properties [N_PROPS]; +static guint signals [N_SIGNALS]; + +static void +dzl_shortcut_theme_editor_dialog_response (DzlShortcutThemeEditor *self, + gint response_code, + DzlShortcutAccelDialog *dialog) +{ + DzlShortcutThemeEditorPrivate *priv = dzl_shortcut_theme_editor_get_instance_private (self); + gboolean changed = FALSE; + + g_assert (DZL_IS_SHORTCUT_THEME_EDITOR (self)); + g_assert (DZL_SHORTCUT_ACCEL_DIALOG (dialog)); + + if (response_code == GTK_RESPONSE_ACCEPT) + { + const DzlShortcutChord *chord = dzl_shortcut_accel_dialog_get_chord (dialog); + + if (priv->selected != NULL) + { + GtkTreePath *path; + GtkTreeModel *model; + GtkTreeIter iter; + + model = gtk_tree_view_get_model (priv->tree_view); + + if (GTK_IS_TREE_STORE (model)) + path = gtk_tree_path_copy (priv->selected); + else + path = gtk_tree_model_filter_convert_path_to_child_path (GTK_TREE_MODEL_FILTER (model), priv->selected); + + if (gtk_tree_model_get_iter (model, &iter, path)) + dzl_shortcut_model_set_chord (DZL_SHORTCUT_MODEL (priv->model), &iter, chord); + } + + changed = TRUE; + } + + gtk_widget_destroy (GTK_WIDGET (dialog)); + + if (changed) + g_signal_emit (self, signals [CHANGED], 0); +} + +static gboolean +dzl_shortcut_theme_editor_visible_func (GtkTreeModel *model, + GtkTreeIter *iter, + gpointer user_data) +{ + const gchar *text = user_data; + g_autofree gchar *keywords= NULL; + GtkTreeIter parent; + + g_assert (GTK_IS_TREE_MODEL (model)); + g_assert (iter != NULL); + g_assert (text != NULL); + + if (!gtk_tree_model_iter_parent (model, &parent, iter)) + return TRUE; + + gtk_tree_model_get (model, iter, + DZL_SHORTCUT_MODEL_COLUMN_KEYWORDS, &keywords, + -1); + + /* keywords and text are both casefolded */ + if (strstr (keywords, text) != NULL) + return TRUE; + + return FALSE; +} + +static void +dzl_shortcut_theme_editor_filter_changed (DzlShortcutThemeEditor *self, + GtkSearchEntry *entry) +{ + DzlShortcutThemeEditorPrivate *priv = dzl_shortcut_theme_editor_get_instance_private (self); + g_autoptr(GtkTreeModel) filter = NULL; + const gchar *text; + + g_assert (DZL_IS_SHORTCUT_THEME_EDITOR (self)); + g_assert (GTK_IS_SEARCH_ENTRY (entry)); + + filter = gtk_tree_model_filter_new (priv->model, NULL); + text = gtk_entry_get_text (GTK_ENTRY (entry)); + + if (dzl_str_empty0 (text)) + { + gtk_tree_view_set_model (priv->tree_view, priv->model); + gtk_tree_view_expand_all (priv->tree_view); + return; + } + + gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (filter), + dzl_shortcut_theme_editor_visible_func, + g_utf8_casefold (text, -1), + g_free); + gtk_tree_view_set_model (priv->tree_view, GTK_TREE_MODEL (filter)); + gtk_tree_view_expand_all (priv->tree_view); +} + +static void +dzl_shortcut_theme_editor_row_activated (DzlShortcutThemeEditor *self, + GtkTreePath *tree_path, + GtkTreeViewColumn *column, + GtkTreeView *tree_view) +{ + DzlShortcutThemeEditorPrivate *priv = dzl_shortcut_theme_editor_get_instance_private (self); + GtkTreeModel *model; + GtkTreeIter iter; + + g_assert (DZL_IS_SHORTCUT_THEME_EDITOR (self)); + g_assert (GTK_IS_TREE_VIEW (tree_view)); + g_assert (tree_path != NULL); + g_assert (GTK_IS_TREE_VIEW_COLUMN (column)); + + if (gtk_tree_path_get_depth (tree_path) == 1) + return; + + model = gtk_tree_view_get_model (tree_view); + + if (gtk_tree_model_get_iter (model, &iter, tree_path)) + { + g_autofree gchar *title = NULL; + g_autofree gchar *accel = NULL; + GtkDialog *dialog; + GtkWidget *toplevel; + + g_clear_pointer (&priv->selected, gtk_tree_path_free); + priv->selected = gtk_tree_path_copy (tree_path); + + gtk_tree_model_get (model, &iter, + DZL_SHORTCUT_MODEL_COLUMN_TITLE, &title, + DZL_SHORTCUT_MODEL_COLUMN_ACCEL, &accel, + -1); + + toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW); + + dialog = g_object_new (DZL_TYPE_SHORTCUT_ACCEL_DIALOG, + "modal", TRUE, + "resizable", FALSE, + "shortcut-title", title, + "title", _("Set Shortcut"), + "transient-for", toplevel, + "use-header-bar", TRUE, + NULL); + + g_signal_connect_object (dialog, + "response", + G_CALLBACK (dzl_shortcut_theme_editor_dialog_response), + self, + G_CONNECT_SWAPPED); + + gtk_window_present (GTK_WINDOW (dialog)); + } +} + +static void +shortcut_cell_data_func (GtkCellLayout *cell_layout, + GtkCellRenderer *renderer, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer user_data) +{ + DzlShortcutThemeEditor *self = user_data; + g_autofree gchar *accel = NULL; + GtkTreeIter piter; + + g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout)); + g_assert (GTK_IS_CELL_RENDERER (renderer)); + g_assert (GTK_IS_TREE_MODEL (model)); + g_assert (iter != NULL); + g_assert (DZL_IS_SHORTCUT_THEME_EDITOR (self)); + + gtk_tree_model_get (model, iter, + DZL_SHORTCUT_MODEL_COLUMN_ACCEL, &accel, + -1); + + if (accel && *accel) + g_object_set (renderer, "text", accel, NULL); + else if (gtk_tree_model_iter_parent (model, &piter, iter)) + g_object_set (renderer, "text", "Disabled", NULL); + else + g_object_set (renderer, "text", NULL, NULL); +} + +static void +title_cell_data_func (GtkCellLayout *cell_layout, + GtkCellRenderer *renderer, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer user_data) +{ + DzlShortcutThemeEditor *self = user_data; + DzlShortcutThemeEditorPrivate *priv = dzl_shortcut_theme_editor_get_instance_private (self); + g_autofree gchar *title = NULL; + GtkTreeIter piter; + + g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout)); + g_assert (GTK_IS_CELL_RENDERER (renderer)); + g_assert (GTK_IS_TREE_MODEL (model)); + g_assert (iter != NULL); + g_assert (DZL_IS_SHORTCUT_THEME_EDITOR (self)); + + gtk_tree_model_get (model, iter, + DZL_SHORTCUT_MODEL_COLUMN_TITLE, &title, + -1); + + g_object_set (renderer, "text", title, NULL); + + if (!gtk_tree_model_iter_parent (model, &piter, iter)) + g_object_set (renderer, "attributes", priv->attrs, NULL); + else + g_object_set (renderer, "attributes", NULL, NULL); +} + +static void +dzl_shortcut_theme_editor_changed (DzlShortcutThemeEditor *self, + DzlShortcutManager *manager) +{ + DzlShortcutThemeEditorPrivate *priv = dzl_shortcut_theme_editor_get_instance_private (self); + + g_assert (DZL_IS_SHORTCUT_THEME_EDITOR (self)); + g_assert (DZL_IS_SHORTCUT_MANAGER (manager)); + + dzl_shortcut_model_rebuild (DZL_SHORTCUT_MODEL (priv->model)); + gtk_tree_view_expand_all (priv->tree_view); +} + +static void +dzl_shortcut_theme_editor_finalize (GObject *object) +{ + DzlShortcutThemeEditor *self = (DzlShortcutThemeEditor *)object; + DzlShortcutThemeEditorPrivate *priv = dzl_shortcut_theme_editor_get_instance_private (self); + + g_clear_object (&priv->model); + g_clear_object (&priv->theme); + g_clear_pointer (&priv->selected, gtk_tree_path_free); + g_clear_pointer (&priv->attrs, pango_attr_list_unref); + + G_OBJECT_CLASS (dzl_shortcut_theme_editor_parent_class)->finalize (object); +} + +static void +dzl_shortcut_theme_editor_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutThemeEditor *self = DZL_SHORTCUT_THEME_EDITOR (object); + + switch (prop_id) + { + case PROP_THEME: + g_value_set_object (value, dzl_shortcut_theme_editor_get_theme (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_theme_editor_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutThemeEditor *self = DZL_SHORTCUT_THEME_EDITOR (object); + + switch (prop_id) + { + case PROP_THEME: + dzl_shortcut_theme_editor_set_theme (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_theme_editor_class_init (DzlShortcutThemeEditorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_shortcut_theme_editor_finalize; + object_class->get_property = dzl_shortcut_theme_editor_get_property; + object_class->set_property = dzl_shortcut_theme_editor_set_property; + + properties [PROP_THEME] = + g_param_spec_object ("theme", + "Theme", + "The theme for editing", + DZL_TYPE_SHORTCUT_THEME, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + /** + * DzlShortcutThemeEditor::changed: + * + * The "changed" signal is emitted when one of the rows within the editor + * has been changed. + * + * You might want to use this signal to save your theme changes to your + * configured storage backend. + */ + signals [CHANGED] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-shortcut-theme-editor.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, DzlShortcutThemeEditor, tree_view); + gtk_widget_class_bind_template_child_private (widget_class, DzlShortcutThemeEditor, filter_entry); + gtk_widget_class_bind_template_child_private (widget_class, DzlShortcutThemeEditor, shortcut_cell); + gtk_widget_class_bind_template_child_private (widget_class, DzlShortcutThemeEditor, shortcut_column); + gtk_widget_class_bind_template_child_private (widget_class, DzlShortcutThemeEditor, title_cell); + gtk_widget_class_bind_template_child_private (widget_class, DzlShortcutThemeEditor, title_column); +} + +static void +dzl_shortcut_theme_editor_init (DzlShortcutThemeEditor *self) +{ + DzlShortcutThemeEditorPrivate *priv = dzl_shortcut_theme_editor_get_instance_private (self); + PangoAttrList *list = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + priv->model = dzl_shortcut_model_new (); + gtk_tree_view_set_model (priv->tree_view, priv->model); + + g_signal_connect_object (dzl_shortcut_manager_get_default (), + "changed", + G_CALLBACK (dzl_shortcut_theme_editor_changed), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (priv->filter_entry, + "changed", + G_CALLBACK (dzl_shortcut_theme_editor_filter_changed), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (priv->tree_view, + "row-activated", + G_CALLBACK (dzl_shortcut_theme_editor_row_activated), + self, + G_CONNECT_SWAPPED); + + /* Set "dim-label" like alpha on the shortcut label */ + list = pango_attr_list_new (); + pango_attr_list_insert (list, pango_attr_foreground_alpha_new (0.55 * G_MAXUSHORT)); + g_object_set (priv->shortcut_cell, + "attributes", list, + NULL); + pango_attr_list_unref (list); + + /* Setup cell data funcs */ + gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (priv->title_column), + GTK_CELL_RENDERER (priv->title_cell), + title_cell_data_func, + self, + NULL); + gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (priv->shortcut_column), + GTK_CELL_RENDERER (priv->shortcut_cell), + shortcut_cell_data_func, + self, + NULL); + + /* diable selections on the treeview */ + gtk_tree_selection_set_mode (gtk_tree_view_get_selection (priv->tree_view), + GTK_SELECTION_NONE); + + priv->attrs = pango_attr_list_new (); + pango_attr_list_insert (priv->attrs, pango_attr_foreground_alpha_new (0.55 * 0xFFFF)); +} + +GtkWidget * +dzl_shortcut_theme_editor_new (void) +{ + return g_object_new (DZL_TYPE_SHORTCUT_THEME_EDITOR, NULL); +} + +/** + * dzl_shortcut_theme_editor_get_theme: + * @self: a #DzlShortcutThemeEditor + * + * Gets the shortcut theme if one hsa been set. + * + * Returns: (transfer none) (nullable): An #DzlShortcutTheme or %NULL + */ +DzlShortcutTheme * +dzl_shortcut_theme_editor_get_theme (DzlShortcutThemeEditor *self) +{ + DzlShortcutThemeEditorPrivate *priv = dzl_shortcut_theme_editor_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME_EDITOR (self), NULL); + + return priv->theme; +} + +void +dzl_shortcut_theme_editor_set_theme (DzlShortcutThemeEditor *self, + DzlShortcutTheme *theme) +{ + DzlShortcutThemeEditorPrivate *priv = dzl_shortcut_theme_editor_get_instance_private (self); + + g_return_if_fail (DZL_IS_SHORTCUT_THEME_EDITOR (self)); + g_return_if_fail (!theme || DZL_IS_SHORTCUT_THEME (theme)); + + if (g_set_object (&priv->theme, theme)) + { + dzl_shortcut_model_set_theme (DZL_SHORTCUT_MODEL (priv->model), theme); + gtk_tree_view_expand_all (priv->tree_view); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME]); + } +} diff --git a/src/shortcuts/dzl-shortcut-theme-editor.h b/src/shortcuts/dzl-shortcut-theme-editor.h new file mode 100644 index 0000000..f12f2d1 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-theme-editor.h @@ -0,0 +1,59 @@ +/* dzl-shortcut-theme-editor.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#ifndef DZL_SHORTCUT_THEME_EDITOR_H +#define DZL_SHORTCUT_THEME_EDITOR_H + +#include + +#include "dzl-version-macros.h" + +#include "dzl-shortcut-theme.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUT_THEME_EDITOR (dzl_shortcut_theme_editor_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlShortcutThemeEditor, dzl_shortcut_theme_editor, DZL, SHORTCUT_THEME_EDITOR, GtkBin) + +struct _DzlShortcutThemeEditorClass +{ + GtkBinClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_shortcut_theme_editor_new (void); +DZL_AVAILABLE_IN_ALL +DzlShortcutTheme *dzl_shortcut_theme_editor_get_theme (DzlShortcutThemeEditor *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_theme_editor_set_theme (DzlShortcutThemeEditor *self, + DzlShortcutTheme *theme); + +G_END_DECLS + +#endif /* DZL_SHORTCUT_THEME_EDITOR_H */ diff --git a/src/shortcuts/dzl-shortcut-theme-editor.ui b/src/shortcuts/dzl-shortcut-theme-editor.ui new file mode 100644 index 0000000..9cb9986 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-theme-editor.ui @@ -0,0 +1,83 @@ + + + + diff --git a/src/shortcuts/dzl-shortcut-theme-load.c b/src/shortcuts/dzl-shortcut-theme-load.c new file mode 100644 index 0000000..25f4c3e --- /dev/null +++ b/src/shortcuts/dzl-shortcut-theme-load.c @@ -0,0 +1,764 @@ +/* dzl-shortcut-theme-load.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-theme" + +#include "config.h" + +#include + +#include "shortcuts/dzl-shortcut-context.h" +#include "shortcuts/dzl-shortcut-private.h" +#include "shortcuts/dzl-shortcut-theme.h" + +typedef enum +{ + LOAD_STATE_THEME = 1, + LOAD_STATE_CONTEXT, + LOAD_STATE_PROPERTY, + LOAD_STATE_SHORTCUT, + LOAD_STATE_SIGNAL, + LOAD_STATE_PARAM, + LOAD_STATE_ACTION, +} LoadStateType; + +typedef struct _LoadStateFrame +{ + LoadStateType type; + + /* Owned references */ + struct _LoadStateFrame *next; + DzlShortcutContext *context; + gchar *accelerator; + gchar *signal; + GSList *params; + + /* Weak references */ + GObject *object; + GParamSpec *pspec; + + guint translatable : 1; +} LoadStateFrame; + +typedef struct +{ + DzlShortcutTheme *self; + LoadStateFrame *stack; + GString *text; + const gchar *translation_domain; + guint in_param : 1; + guint in_property : 1; +} LoadState; + +static LoadStateFrame * +load_state_frame_new (LoadStateType type) +{ + LoadStateFrame *frm; + + frm = g_slice_new0 (LoadStateFrame); + frm->type = type; + + return frm; +} + +static void +load_state_frame_free (LoadStateFrame *frm) +{ + g_clear_object (&frm->context); + g_clear_pointer (&frm->accelerator, g_free); + g_clear_pointer (&frm->signal, g_free); + + g_slist_free_full (frm->params, g_free); + frm->params = NULL; + + g_slice_free (LoadStateFrame, frm); +} + +static void +load_state_push (LoadState *state, + LoadStateFrame *frm) +{ + g_assert (state != NULL); + g_assert (frm != NULL); + g_assert (frm->next == NULL); + + frm->next = state->stack; + state->stack = frm; +} + +static gboolean +load_state_check_type (LoadState *state, + LoadStateType type, + GError **error) +{ + if (state->stack != NULL) + { + if (state->stack->type == type) + return TRUE; + } + + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Unexpected stack when unwinding elements"); + + return FALSE; +} + +static void +load_state_pop (LoadState *state) +{ + LoadStateFrame *frm = state->stack; + + if (frm != NULL) + { + state->stack = frm->next; + load_state_frame_free (frm); + } +} + +static void +load_state_add_action (LoadState *state, + const gchar *action) +{ + DzlShortcutContext *context = NULL; + DzlShortcutTheme *theme = NULL; + const gchar *accel = NULL; + + g_assert (state != NULL); + g_assert (action != NULL); + + /* NOTE: Keep this in sync with load_state_add_command() */ + + for (LoadStateFrame *iter = state->stack; iter != NULL; iter = iter->next) + { + if (iter->type == LOAD_STATE_SHORTCUT) + accel = iter->accelerator; + else if (iter->type == LOAD_STATE_CONTEXT) + context = iter->context; + else if (iter->type == LOAD_STATE_THEME) + theme = state->self; + + if (accel && (context || theme)) + break; + } + + if (accel != NULL) + { + if (context != NULL) + dzl_shortcut_context_add_action (context, accel, action); + else if (theme != NULL) + dzl_shortcut_theme_set_accel_for_action (theme, action, accel, 0); + } +} + +static void +load_state_add_command (LoadState *state, + const gchar *command) +{ + DzlShortcutContext *context = NULL; + DzlShortcutTheme *theme = NULL; + const gchar *accel = NULL; + + g_assert (state != NULL); + g_assert (command != NULL); + + /* NOTE: Keep this in sync with load_state_add_action() */ + + for (LoadStateFrame *iter = state->stack; iter != NULL; iter = iter->next) + { + if (iter->type == LOAD_STATE_SHORTCUT) + accel = iter->accelerator; + else if (iter->type == LOAD_STATE_CONTEXT) + context = iter->context; + else if (iter->type == LOAD_STATE_THEME) + theme = state->self; + + if (accel && (context || theme)) + break; + } + + if (accel != NULL) + { + if (context != NULL) + dzl_shortcut_context_add_command (context, accel, command); + else if (theme != NULL) + dzl_shortcut_theme_set_accel_for_command (theme, command, accel, 0); + } +} + +static void +load_state_commit_param (LoadState *state) +{ + gchar *text; + + g_assert (state->stack != NULL); + g_assert (state->stack->type == LOAD_STATE_SIGNAL); + g_assert (state->text != NULL); + + text = g_string_free (state->text, FALSE); + state->text = NULL; + state->stack->params = g_slist_append (state->stack->params, text); +} + +static void +load_state_commit_property (LoadState *state, + GError **error) +{ + g_auto(GValue) value = G_VALUE_INIT; + + g_assert (state->stack != NULL); + g_assert (state->stack->type == LOAD_STATE_PROPERTY); + g_assert (state->stack->pspec != NULL); + g_assert (state->text != NULL); + + /* XXX: Note this isn't super safe, since we are passing a NULL + * GtkBuilder, but it does work for the cases we need to support. + * But there is the chance for a NULL dereference that we should + * probably protect against. + */ + if (gtk_builder_value_from_string_type (NULL, + G_PARAM_SPEC_VALUE_TYPE (state->stack->pspec), + state->text->str, + &value, + error)) + g_object_set_property (state->stack->object, + state->stack->pspec->name, + &value); + + g_string_free (state->text, TRUE); + state->text = NULL; +} + +static void +parse_into_value (const gchar *str, + GValue *value) +{ + g_autofree gchar *lower = NULL; + + /* + * We don't know the type at this point, so we rely on various + * GValueTransform to convert types at runtime upon signal emission. It adds + * some runtime overhead but allows more flexibility in where we emit + * signals from shortcuts. + */ + + if (!str || !*str) + { + g_value_init (value, G_TYPE_STRING); + return; + } + + if (g_ascii_isdigit (*str) || *str == '-' || *str == '+') + { + if (strchr (str, '.') != NULL) + { + g_value_init (value, G_TYPE_DOUBLE); + g_value_set_double (value, g_ascii_strtod (str, NULL)); + } + else + { + gint64 v = g_ascii_strtoll (str, NULL, 10); + + if (ABS (v) <= G_MAXINT) + { + g_value_init (value, G_TYPE_INT); + g_value_set_int (value, v); + } + else + { + g_value_init (value, G_TYPE_INT64); + g_value_set_int64 (value, v); + } + } + + return; + } + + lower = g_utf8_strdown (str, -1); + + if (g_str_equal (lower, "false")) + { + g_value_init (value, G_TYPE_BOOLEAN); + return; + } + + if (g_str_equal (lower, "true")) + { + g_value_init (value, G_TYPE_BOOLEAN); + g_value_set_boolean (value, TRUE); + return; + } + + g_value_init (value, G_TYPE_STRING); + g_value_set_string (value, str); +} + +static void +load_state_add_signal (LoadState *state) +{ + LoadStateFrame *signal; + LoadStateFrame *shortcut; + LoadStateFrame *context; + g_autoptr(GArray) values = NULL; + + g_assert (state->stack != NULL); + g_assert (state->stack->type == LOAD_STATE_SIGNAL); + g_assert (state->stack->next != NULL); + g_assert (state->stack->next->type == LOAD_STATE_SHORTCUT); + g_assert (state->stack->next->accelerator != NULL); + g_assert (state->stack->next->next->type == LOAD_STATE_CONTEXT); + g_assert (state->stack->next->next->context != NULL); + + signal = state->stack; + shortcut = signal->next; + context = shortcut->next; + + g_assert (signal->type == LOAD_STATE_SIGNAL); + g_assert (shortcut->type == LOAD_STATE_SHORTCUT); + g_assert (context->type == LOAD_STATE_CONTEXT); + + values = g_array_sized_new (FALSE, FALSE, sizeof (GValue), g_slist_length (signal->params)); + g_array_set_clear_func (values, (GDestroyNotify)g_value_unset); + + for (const GSList *iter = signal->params; iter != NULL; iter = iter->next) + { + const gchar *str = iter->data; + GValue value = G_VALUE_INIT; + + parse_into_value (str, &value); + + g_array_append_val (values, value); + } + +#if 0 + g_print ("Adding signal %s to %s via %s\n", + signal->signal, + dzl_shortcut_context_get_name (context->context), + shortcut->accelerator); +#endif + + dzl_shortcut_context_add_signalv (context->context, + shortcut->accelerator, + signal->signal, + values); +} + +static void +theme_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attr_names, + const gchar **attr_values, + gpointer user_data, + GError **error) +{ + LoadState *state = user_data; + + g_assert (state != NULL); + g_assert (DZL_IS_SHORTCUT_THEME (state->self)); + g_assert (context != NULL); + g_assert (element_name != NULL); + + if (g_strcmp0 (element_name, "keytheme") == 0) + { + const gchar *name = NULL; + const gchar *parent = NULL; + const gchar *domain = NULL; + + if (state->stack != NULL) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Got theme element in location other than root"); + return; + } + + if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error, + G_MARKUP_COLLECT_STRING, "name", &name, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "parent", &parent, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "translation-domain", &domain, + G_MARKUP_COLLECT_INVALID)) + return; + + if (domain != NULL) + state->translation_domain = g_intern_string (domain); + + _dzl_shortcut_theme_set_name (state->self, name); + + if (parent != NULL) + dzl_shortcut_theme_set_parent_name (state->self, parent); + + load_state_push (state, load_state_frame_new (LOAD_STATE_THEME)); + } + else if (g_strcmp0 (element_name, "property") == 0) + { + LoadStateFrame *frm; + const gchar *translatable = NULL; + const gchar *name = NULL; + GParamSpec *pspec; + GObject *obj = NULL; + + if (!load_state_check_type (state, LOAD_STATE_CONTEXT, NULL) && + !load_state_check_type (state, LOAD_STATE_THEME, NULL)) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "property only valid in theme or context"); + return; + } + + if (state->stack->type == LOAD_STATE_CONTEXT) + obj = G_OBJECT (state->stack->context); + else if (state->stack->type == LOAD_STATE_THEME) + obj = G_OBJECT (state->self); + else { g_assert_not_reached (); } + + if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error, + G_MARKUP_COLLECT_STRING, "name", &name, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "translatable", &translatable, + G_MARKUP_COLLECT_INVALID)) + return; + + pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (obj), name); + + if (pspec == NULL) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Failed to locate “%s” property", + name); + return; + } + + frm = load_state_frame_new (LOAD_STATE_PROPERTY); + frm->pspec = pspec; + frm->object = obj; + frm->translatable = translatable && (*translatable == 'y' || *translatable == 'Y'); + + load_state_push (state, frm); + + state->in_property = TRUE; + } + else if (g_strcmp0 (element_name, "context") == 0) + { + LoadStateFrame *frm; + const gchar *name = NULL; + + if (!load_state_check_type (state, LOAD_STATE_THEME, error)) + return; + + if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error, + G_MARKUP_COLLECT_STRING, "name", &name, + G_MARKUP_COLLECT_INVALID)) + return; + + frm = load_state_frame_new (LOAD_STATE_CONTEXT); + frm->context = dzl_shortcut_context_new (name); + + load_state_push (state, frm); + } + else if (g_strcmp0 (element_name, "shortcut") == 0) + { + LoadStateFrame *frm; + const gchar *accelerator = NULL; + const gchar *action = NULL; + const gchar *signal = NULL; + const gchar *command = NULL; + + if (!load_state_check_type (state, LOAD_STATE_CONTEXT, NULL) && + !load_state_check_type (state, LOAD_STATE_THEME, NULL)) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "shortcut only allowed in context or theme elements"); + return; + } + + if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error, + G_MARKUP_COLLECT_STRING, "accelerator", &accelerator, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "action", &action, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "signal", &signal, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "command", &command, + G_MARKUP_COLLECT_INVALID)) + return; + + frm = load_state_frame_new (LOAD_STATE_SHORTCUT); + frm->accelerator = g_strdup (accelerator); + load_state_push (state, frm); + + if (action != NULL) + load_state_add_action (state, action); + + if (command != NULL) + load_state_add_command (state, command); + + if (signal != NULL) + { + frm = load_state_frame_new (LOAD_STATE_SIGNAL); + frm->signal = g_strdup (signal); + load_state_push (state, frm); + load_state_add_signal (state); + load_state_pop (state); + } + } + else if (g_strcmp0 (element_name, "signal") == 0) + { + LoadStateFrame *frm; + const gchar *name = NULL; + + if (!load_state_check_type (state, LOAD_STATE_SHORTCUT, error)) + return; + + if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error, + G_MARKUP_COLLECT_STRING, "name", &name, + G_MARKUP_COLLECT_INVALID)) + return; + + frm = load_state_frame_new (LOAD_STATE_SIGNAL); + frm->signal = g_strdup (name); + + load_state_push (state, frm); + } + else if (g_strcmp0 (element_name, "param") == 0) + { + if (!load_state_check_type (state, LOAD_STATE_SIGNAL, error)) + return; + + state->in_param = TRUE; + } + else if (g_strcmp0 (element_name, "action") == 0) + { + const gchar *name = NULL; + + if (!load_state_check_type (state, LOAD_STATE_SHORTCUT, error)) + return; + + if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error, + G_MARKUP_COLLECT_STRING, "name", &name, + G_MARKUP_COLLECT_INVALID)) + return; + + load_state_add_action (state, name); + } + else if (g_strcmp0 (element_name, "resource") == 0) + { + const gchar *path = NULL; + g_autofree gchar *full_path = NULL; + + if (!load_state_check_type (state, LOAD_STATE_THEME, error)) + return; + + if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error, + G_MARKUP_COLLECT_STRING, "path", &path, + G_MARKUP_COLLECT_INVALID)) + return; + + g_assert (state->self != NULL); + + if (!g_str_has_prefix (path, "resource://")) + path = full_path = g_strdup_printf ("resource://%s", path); + + dzl_shortcut_theme_add_css_resource (state->self, path); + } +} + +static void +theme_end_element (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + LoadState *state = user_data; + + g_assert (context != NULL); + g_assert (element_name != NULL); + + if (g_strcmp0 (element_name, "keytheme") == 0) + { + if (!load_state_check_type (state, LOAD_STATE_THEME, error)) + return; + } + else if (g_strcmp0 (element_name, "resource") == 0) + { + /* nothing to pop, but we want to propagate any errors */ + load_state_check_type (state, LOAD_STATE_THEME, error); + return; + } + else if (g_strcmp0 (element_name, "property") == 0) + { + if (!load_state_check_type (state, LOAD_STATE_PROPERTY, error)) + return; + + if (state->text) + load_state_commit_property (state, error); + + state->in_property = FALSE; + } + else if (g_strcmp0 (element_name, "context") == 0) + { + if (!load_state_check_type (state, LOAD_STATE_CONTEXT, error)) + return; + + dzl_shortcut_theme_add_context (state->self, state->stack->context); + } + else if (g_strcmp0 (element_name, "shortcut") == 0) + { + if (!load_state_check_type (state, LOAD_STATE_SHORTCUT, error)) + return; + } + else if (g_strcmp0 (element_name, "signal") == 0) + { + if (!load_state_check_type (state, LOAD_STATE_SIGNAL, error)) + return; + + load_state_add_signal (state); + } + else if (g_strcmp0 (element_name, "param") == 0) + { + if (!load_state_check_type (state, LOAD_STATE_SIGNAL, error)) + return; + + g_assert (state->in_param); + + if (state->text) + load_state_commit_param (state); + + state->in_param = FALSE; + + return; + } + else if (g_strcmp0 (element_name, "action") == 0) + { + load_state_check_type (state, LOAD_STATE_SHORTCUT, error); + return; + } + else + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Unexpected close element %s", + element_name); + return; + } + + load_state_pop (state); +} + +static void +theme_text (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + LoadState *state = user_data; + + g_assert (context != NULL); + g_assert (text != NULL); + g_assert (state != NULL); + + if (state->in_param || state->in_property) + { + if ((state->in_param && !load_state_check_type (state, LOAD_STATE_SIGNAL, error)) || + (state->in_property && !load_state_check_type (state, LOAD_STATE_PROPERTY, error))) + return; + + if (state->text == NULL) + state->text = g_string_new (NULL); + + g_string_append_len (state->text, text, text_len); + } +} + +static const GMarkupParser theme_parser = { + .start_element = theme_start_element, + .end_element = theme_end_element, + .text = theme_text, +}; + +gboolean +dzl_shortcut_theme_load_from_data (DzlShortcutTheme *self, + const gchar *data, + gssize len, + GError **error) +{ + g_autoptr(GMarkupParseContext) context = NULL; + LoadState state = { 0 }; + gboolean ret; + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + + state.self = self; + + context = g_markup_parse_context_new (&theme_parser, 0, &state, NULL); + ret = g_markup_parse_context_parse (context, data, len, error); + + while (state.stack != NULL) + { + LoadStateFrame *frm = state.stack; + state.stack = frm->next; + load_state_frame_free (frm); + } + + if (state.text) + g_string_free (state.text, TRUE); + + return ret; +} + +gboolean +dzl_shortcut_theme_load_from_file (DzlShortcutTheme *self, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *contents = NULL; + gsize len = 0; + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), FALSE); + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + if (!g_file_load_contents (file, cancellable, &contents, &len, NULL, error)) + return FALSE; + + return dzl_shortcut_theme_load_from_data (self, contents, len, error); +} + +gboolean +dzl_shortcut_theme_load_from_path (DzlShortcutTheme *self, + const gchar *path, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFile) file = NULL; + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), FALSE); + g_return_val_if_fail (path != NULL, FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + file = g_file_new_for_path (path); + + return dzl_shortcut_theme_load_from_file (self, file, cancellable, error); +} diff --git a/src/shortcuts/dzl-shortcut-theme-save.c b/src/shortcuts/dzl-shortcut-theme-save.c new file mode 100644 index 0000000..6574205 --- /dev/null +++ b/src/shortcuts/dzl-shortcut-theme-save.c @@ -0,0 +1,204 @@ +/* dzl-shortcut-theme-save.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 2 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-theme-save" + +#include "config.h" + +#include "shortcuts/dzl-shortcut-theme.h" +#include "shortcuts/dzl-shortcut-private.h" + +gboolean +dzl_shortcut_theme_save_to_stream (DzlShortcutTheme *self, + GOutputStream *stream, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GString) str = NULL; + DzlShortcutContext *context; + GHashTable *contexts; + GHashTableIter iter; + const gchar *name; + const gchar *parent; + const gchar *title; + const gchar *subtitle; + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), FALSE); + g_return_val_if_fail (G_IS_OUTPUT_STREAM (stream), FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + contexts = _dzl_shortcut_theme_get_contexts (self); + + str = g_string_new ("\n"); + + name = dzl_shortcut_theme_get_name (self); + parent = dzl_shortcut_theme_get_parent_name (self); + title = dzl_shortcut_theme_get_title (self); + subtitle = dzl_shortcut_theme_get_subtitle (self); + + if (parent != NULL && !g_str_equal (parent, "internal")) + g_string_append_printf (str, "\n", name, parent); + else + g_string_append_printf (str, "\n", name); + + g_string_append_printf (str, " %s\n", title ? title : ""); + g_string_append_printf (str, " %s\n", subtitle ? subtitle : ""); + + g_hash_table_iter_init (&iter, contexts); + + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&context)) + { + DzlShortcutChordTable *table; + DzlShortcutChordTableIter citer; + gboolean use_binding_sets = FALSE; + const DzlShortcutChord *chord = NULL; + DzlShortcutClosureChain *chain = NULL; + + table = _dzl_shortcut_context_get_table (context); + name = dzl_shortcut_context_get_name (context); + g_object_get (context, "use-binding-sets", &use_binding_sets, NULL); + + g_string_append_printf (str, " \n", name); + + if (!use_binding_sets) + g_string_append (str, " false\n"); + + _dzl_shortcut_chord_table_iter_init (&citer, table); + + while (_dzl_shortcut_chord_table_iter_next (&citer, &chord, (gpointer *)&chain)) + { + g_autofree gchar *accel = dzl_shortcut_chord_to_string (chord); + + if (chain == NULL || accel == NULL) + continue; + + g_string_append_printf (str, " \n", accel); + + for (const GSList *node = &chain->node; node != NULL; node = node->next) + { + chain = node->data; + + if (chain->type == DZL_SHORTCUT_CLOSURE_ACTION) + { + if (chain->action.params == NULL) + { + g_string_append_printf (str, " \n", + chain->action.group, chain->action.name); + } + else + { + g_autofree gchar *fmt = g_variant_print (chain->action.params, FALSE); + g_string_append_printf (str, " \n", + chain->action.group, chain->action.name, fmt); + } + } + else if (chain->type == DZL_SHORTCUT_CLOSURE_SIGNAL) + { + if (chain->signal.detail) + g_string_append_printf (str, " signal.name, + g_quark_to_string (chain->signal.detail)); + else + g_string_append_printf (str, " signal.name); + + if (chain->signal.params == NULL || chain->signal.params->len == 0) + { + g_string_append (str, "/>\n"); + continue; + } + + g_string_append (str, ">\n"); + + for (guint j = 0; j < chain->signal.params->len; j++) + { + GValue *value = &g_array_index (chain->signal.params, GValue, j); + + if (G_VALUE_HOLDS_STRING (value)) + { + g_autofree gchar *escape = g_markup_escape_text (g_value_get_string (value), -1); + + g_string_append_printf (str, " \"%s\"\n", escape); + } + else + { + g_auto(GValue) translated = G_VALUE_INIT; + + g_value_init (&translated, G_TYPE_STRING); + g_value_transform (value, &translated); + g_string_append_printf (str, " %s\n", g_value_get_string (&translated)); + } + } + + g_string_append (str, " \n"); + + } + } + + g_string_append (str, " \n"); + } + + g_string_append (str, " \n"); + } + + g_string_append (str, "\n"); + + return g_output_stream_write_all (stream, str->str, str->len, NULL, cancellable, error); +} + +gboolean +dzl_shortcut_theme_save_to_file (DzlShortcutTheme *self, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFileOutputStream) stream = NULL; + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), FALSE); + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + stream = g_file_replace (file, + NULL, + FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, + cancellable, + error); + + if (stream == NULL) + return FALSE; + + return dzl_shortcut_theme_save_to_stream (self, G_OUTPUT_STREAM (stream), cancellable, error); +} + +gboolean +dzl_shortcut_theme_save_to_path (DzlShortcutTheme *self, + const gchar *path, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFile) file = NULL; + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), FALSE); + g_return_val_if_fail (path != NULL, FALSE); + g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE); + + file = g_file_new_for_path (path); + + return dzl_shortcut_theme_save_to_file (self, file, cancellable, error); +} diff --git a/src/shortcuts/dzl-shortcut-theme.c b/src/shortcuts/dzl-shortcut-theme.c new file mode 100644 index 0000000..0cda36c --- /dev/null +++ b/src/shortcuts/dzl-shortcut-theme.c @@ -0,0 +1,983 @@ +/* dzl-shortcut-theme.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 2 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 . + */ + +#define G_LOG_DOMAIN "dzl-shortcut-theme" + +#include "config.h" + +#include + +#include "dzl-debug.h" + +#include "shortcuts/dzl-shortcut-private.h" +#include "shortcuts/dzl-shortcut-chord.h" +#include "shortcuts/dzl-shortcut-theme.h" + +typedef struct +{ + gchar *name; + gchar *title; + gchar *subtitle; + + /* + * The parent_name property can be used to inherit from another + * shortcut theme when dispatching operations. The controllers + * will use this to locate the parent theme/context pair and + * try after the active theme fails to dispatch. + */ + gchar *parent_name; + + /* + * A hashtable of context names to the context pointer. The hashtable + * owns the reference to the context. + */ + GHashTable *contexts; + + /* + * A list of additional CSS resources that should be ingreated with this + * theme so that everything is applied together. You might use this if + * some of your keytheme needs to use CSS keybinding resources. + */ + GHashTable *resource_providers; + + /* + * Commands and actions can be mapped from a context or directly from the + * theme for convenience (to avoid having to define them from every context). + */ + DzlShortcutChordTable *actions_table; + DzlShortcutChordTable *commands_table; + + /* + * Weak back-pointer to the DzlShorcutMangaer that owns this theme. A theme + * can only be in one manager at a time. This will be cleared when the theme + * is removed from the manager. + */ + DzlShortcutManager *manager; + + /* + * The theme also maintains a list of chains overridden by the theme. These + * are indexed by the interned string pointer (for direct pointer copmarison) + * of the command_id or action_id. + */ + GHashTable *chains; +} DzlShortcutThemePrivate; + +enum { + PROP_0, + PROP_NAME, + PROP_PARENT_NAME, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlShortcutTheme, dzl_shortcut_theme, G_TYPE_OBJECT) + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_shortcut_theme_finalize (GObject *object) +{ + DzlShortcutTheme *self = (DzlShortcutTheme *)object; + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_clear_pointer (&priv->name, g_free); + g_clear_pointer (&priv->parent_name, g_free); + g_clear_pointer (&priv->title, g_free); + g_clear_pointer (&priv->subtitle, g_free); + g_clear_pointer (&priv->contexts, g_hash_table_unref); + g_clear_pointer (&priv->chains, g_hash_table_unref); + g_clear_pointer (&priv->actions_table, dzl_shortcut_chord_table_free); + g_clear_pointer (&priv->commands_table, dzl_shortcut_chord_table_free); + + G_OBJECT_CLASS (dzl_shortcut_theme_parent_class)->finalize (object); +} + +static void +dzl_shortcut_theme_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutTheme *self = (DzlShortcutTheme *)object; + + switch (prop_id) + { + case PROP_NAME: + g_value_set_string (value, dzl_shortcut_theme_get_name (self)); + break; + + case PROP_PARENT_NAME: + g_value_set_string (value, dzl_shortcut_theme_get_parent_name (self)); + break; + + case PROP_TITLE: + g_value_set_string (value, dzl_shortcut_theme_get_title (self)); + break; + + case PROP_SUBTITLE: + g_value_set_string (value, dzl_shortcut_theme_get_subtitle (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_theme_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutTheme *self = (DzlShortcutTheme *)object; + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + switch (prop_id) + { + case PROP_NAME: + priv->name = g_value_dup_string (value); + break; + + case PROP_PARENT_NAME: + dzl_shortcut_theme_set_parent_name (self, g_value_get_string (value)); + break; + + case PROP_TITLE: + g_free (priv->title); + priv->title = g_value_dup_string (value); + break; + + case PROP_SUBTITLE: + g_free (priv->subtitle); + priv->subtitle = g_value_dup_string (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcut_theme_class_init (DzlShortcutThemeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_shortcut_theme_finalize; + object_class->get_property = dzl_shortcut_theme_get_property; + object_class->set_property = dzl_shortcut_theme_set_property; + + properties [PROP_NAME] = + g_param_spec_string ("name", + "Name", + "The name of the theme", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_PARENT_NAME] = + g_param_spec_string ("parent-name", + "Parent Name", + "The name of the parent shortcut theme", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "The title of the theme as used for UI elements", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SUBTITLE] = + g_param_spec_string ("subtitle", + "Subtitle", + "The subtitle of the theme as used for UI elements", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_shortcut_theme_init (DzlShortcutTheme *self) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + priv->commands_table = dzl_shortcut_chord_table_new (); + priv->actions_table = dzl_shortcut_chord_table_new (); + priv->contexts = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref); + priv->chains = g_hash_table_new_full (NULL, NULL, NULL, + (GDestroyNotify)dzl_shortcut_closure_chain_free); +} + +const gchar * +dzl_shortcut_theme_get_name (DzlShortcutTheme *self) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + + return priv->name; +} + +/** + * dzl_shortcut_theme_find_context_by_name: + * @self: An #DzlShortcutContext + * @name: The name of the context + * + * Gets the context named @name. If the context does not exist, it will + * be created. + * + * Returns: (not nullable) (transfer none): An #DzlShortcutContext + */ +DzlShortcutContext * +dzl_shortcut_theme_find_context_by_name (DzlShortcutTheme *self, + const gchar *name) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + DzlShortcutContext *ret; + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + g_return_val_if_fail (name != NULL, NULL); + + name = g_intern_string (name); + + if (NULL == (ret = g_hash_table_lookup (priv->contexts, name))) + { + ret = dzl_shortcut_context_new (name); + g_hash_table_insert (priv->contexts, (gchar *)name, ret); + } + + return ret; +} + +/** + * _dzl_shortcut_theme_try_find_context_by_name: + * @self: a #DzlShortcutTheme + * + * This function is like dzl_shortcut_theme_find_context_by_name() but will + * not create the context if it does not exist. + * + * Returns: (transfer none) (nullable): A #DzlShortcutContext or %NULL. + */ +DzlShortcutContext * +_dzl_shortcut_theme_try_find_context_by_name (DzlShortcutTheme *self, + const gchar *name) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + GQuark qname; + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + g_return_val_if_fail (name != NULL, NULL); + + /* Names are interned (which are quarks) */ + if (0 != (qname = g_quark_try_string (name))) + return g_hash_table_lookup (priv->contexts, g_quark_to_string (qname)); + + return NULL; +} + +static DzlShortcutContext * +dzl_shortcut_theme_find_default_context_by_type (DzlShortcutTheme *self, + GType type) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + g_return_val_if_fail (g_type_is_a (type, GTK_TYPE_WIDGET), NULL); + + return dzl_shortcut_theme_find_context_by_name (self, g_type_name (type)); +} + +/** + * dzl_shortcut_theme_find_default_context: + * + * Finds the default context in the theme for @widget. + * + * Returns: (nullable) (transfer none): An #DzlShortcutContext or %NULL. + */ +DzlShortcutContext * +dzl_shortcut_theme_find_default_context (DzlShortcutTheme *self, + GtkWidget *widget) +{ + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL); + + return dzl_shortcut_theme_find_default_context_by_type (self, G_OBJECT_TYPE (widget)); +} + +DzlShortcutContext * +_dzl_shortcut_theme_find_default_context_with_phase (DzlShortcutTheme *self, + GtkWidget *widget, + DzlShortcutPhase phase) +{ + g_autofree gchar *free_me = NULL; + const gchar *name; + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL); + + name = G_OBJECT_TYPE_NAME (widget); + + if ((phase & DZL_SHORTCUT_PHASE_BUBBLE) != 0) + name = free_me = g_strdup_printf ("%s:bubble", name); + else if ((phase & DZL_SHORTCUT_PHASE_CAPTURE) != 0) + name = free_me = g_strdup_printf ("%s:capture", name); + + return dzl_shortcut_theme_find_context_by_name (self, name); +} + +void +dzl_shortcut_theme_add_context (DzlShortcutTheme *self, + DzlShortcutContext *context) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + const gchar *name; + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (context)); + + name = dzl_shortcut_context_get_name (context); + + g_return_if_fail (name != NULL); + + g_hash_table_insert (priv->contexts, (gchar *)g_intern_string (name), g_object_ref (context)); +} + +DzlShortcutTheme * +dzl_shortcut_theme_new (const gchar *name) +{ + return g_object_new (DZL_TYPE_SHORTCUT_THEME, + "name", name, + NULL); +} + +const gchar * +dzl_shortcut_theme_get_title (DzlShortcutTheme *self) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + + return priv->title; +} + +const gchar * +dzl_shortcut_theme_get_subtitle (DzlShortcutTheme *self) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + + return priv->subtitle; +} + +GHashTable * +_dzl_shortcut_theme_get_contexts (DzlShortcutTheme *self) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + + return priv->contexts; +} + +void +_dzl_shortcut_theme_set_name (DzlShortcutTheme *self, + const gchar *name) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + + if (g_strcmp0 (name, priv->name) != 0) + { + g_free (priv->name); + priv->name = g_strdup (name); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NAME]); + } +} + +/** + * dzl_shortcut_theme_get_parent_name: + * @self: a #DzlShortcutTheme + * + * Gets the name of the parent shortcut theme. + * + * This is used to resolve shortcuts from the parent theme without having to + * copy them directly into this shortcut theme. It allows for some level of + * copy-on-write (CoW). + * + * Returns: (nullable): The name of the parent theme, or %NULL if none is set. + */ +const gchar * +dzl_shortcut_theme_get_parent_name (DzlShortcutTheme *self) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + + return priv->parent_name; +} + +void +dzl_shortcut_theme_set_parent_name (DzlShortcutTheme *self, + const gchar *parent_name) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + + if (g_strcmp0 (parent_name, priv->parent_name) != 0) + { + g_free (priv->parent_name); + priv->parent_name = g_strdup (parent_name); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PARENT_NAME]); + } +} + +const gchar * +_dzl_shortcut_theme_lookup_action (DzlShortcutTheme *self, + const DzlShortcutChord *chord) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + g_return_val_if_fail (chord != NULL, NULL); + + if (priv->actions_table != NULL) + { + const gchar *action = NULL; + DzlShortcutMatch match; + + match = dzl_shortcut_chord_table_lookup (priv->actions_table, chord, (gpointer *)&action); + + if (match == DZL_SHORTCUT_MATCH_EQUAL) + return action; + } + + return NULL; +} + +void +dzl_shortcut_theme_set_chord_for_action (DzlShortcutTheme *self, + const gchar *detailed_action_name, + const DzlShortcutChord *chord, + DzlShortcutPhase phase) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + + if (detailed_action_name == NULL) + { + dzl_shortcut_chord_table_remove (priv->actions_table, chord); + return; + } + + detailed_action_name = g_intern_string (detailed_action_name); + + dzl_shortcut_chord_table_remove_data (priv->actions_table, + (gpointer)detailed_action_name); + + if (chord != NULL) + dzl_shortcut_chord_table_add (priv->actions_table, chord, + (gpointer)detailed_action_name); + + if (phase == DZL_SHORTCUT_PHASE_DISPATCH) + phase = DZL_SHORTCUT_PHASE_BUBBLE | DZL_SHORTCUT_PHASE_GLOBAL; + + if (!g_hash_table_contains (priv->chains, detailed_action_name)) + { + DzlShortcutClosureChain *chain; + + chain = dzl_shortcut_closure_chain_append_action_string (NULL, detailed_action_name); + + if (chain != NULL) + { + chain->phase = phase; + g_hash_table_insert (priv->chains, (gchar *)detailed_action_name, chain); + } + } +} + +const DzlShortcutChord * +dzl_shortcut_theme_get_chord_for_action (DzlShortcutTheme *self, + const gchar *detailed_action_name) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + const DzlShortcutChord *ret; + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + + if (priv->actions_table == NULL) + return NULL; + + ret = dzl_shortcut_chord_table_lookup_data (priv->actions_table, + (gpointer)g_intern_string (detailed_action_name)); + + if (ret == NULL) + { + DzlShortcutTheme *parent = dzl_shortcut_theme_get_parent (self); + + if (parent != NULL) + ret = dzl_shortcut_theme_get_chord_for_action (parent, detailed_action_name); + } + + return ret; +} + +void +dzl_shortcut_theme_set_accel_for_action (DzlShortcutTheme *self, + const gchar *detailed_action_name, + const gchar *accel, + DzlShortcutPhase phase) +{ + g_autoptr(DzlShortcutChord) chord = NULL; + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + + if (accel != NULL) + chord = dzl_shortcut_chord_new_from_string (accel); + + dzl_shortcut_theme_set_chord_for_action (self, detailed_action_name, chord, phase); +} + +/** + * dzl_shortcut_theme_set_chord_for_command: + * @self: a #DzlShortcutTheme + * @chord: (nullable): the chord for the command + * @command: (nullable): the command to be executed + * @phase: the phase to activate within, or 0 for the default + * + * This will set the command to execute when @chord is pressed. If command is + * %NULL, the accelerator will be cleared. If @chord is %NULL, all + * accelerators for @command will be cleared. + */ +void +dzl_shortcut_theme_set_chord_for_command (DzlShortcutTheme *self, + const gchar *command, + const DzlShortcutChord *chord, + DzlShortcutPhase phase) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + + if (command == NULL) + { + dzl_shortcut_chord_table_remove (priv->commands_table, chord); + return; + } + + command = g_intern_string (command); + dzl_shortcut_chord_table_remove_data (priv->commands_table, (gpointer)command); + + if (chord != NULL) + dzl_shortcut_chord_table_add (priv->commands_table, chord, (gpointer)command); + + if (phase == DZL_SHORTCUT_PHASE_DISPATCH) + phase = DZL_SHORTCUT_PHASE_BUBBLE | DZL_SHORTCUT_PHASE_GLOBAL; + + if (!g_hash_table_contains (priv->chains, command)) + { + DzlShortcutClosureChain *chain; + + chain = dzl_shortcut_closure_chain_append_command (NULL, command); + chain->phase = phase; + g_hash_table_insert (priv->chains, (gchar *)command, chain); + } +} + +const DzlShortcutChord * +dzl_shortcut_theme_get_chord_for_command (DzlShortcutTheme *self, + const gchar *command) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + const DzlShortcutChord *ret; + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), NULL); + + if (priv->commands_table == NULL) + return NULL; + + ret = dzl_shortcut_chord_table_lookup_data (priv->commands_table, + (gpointer)g_intern_string (command)); + + if (ret == NULL) + { + DzlShortcutTheme *parent = dzl_shortcut_theme_get_parent (self); + + if (parent != NULL) + ret = dzl_shortcut_theme_get_chord_for_command (parent, command); + } + + return ret; +} + +/** + * dzl_shortcut_theme_set_accel_for_command: + * @self: a #DzlShortcutTheme + * @command: (nullable): the command to be executed + * @accel: (nullable): the shortcut accelerator + * @phase: the phase to activate within, or 0 for the default + * + * This will set the command to execute when @accel is pressed. If command is + * %NULL, the accelerator will be cleared. If accelerator is %NULL, all + * accelerators for @command will be cleared. + */ +void +dzl_shortcut_theme_set_accel_for_command (DzlShortcutTheme *self, + const gchar *command, + const gchar *accel, + DzlShortcutPhase phase) +{ + g_autoptr(DzlShortcutChord) chord = NULL; + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + + if (accel != NULL) + chord = dzl_shortcut_chord_new_from_string (accel); + + dzl_shortcut_theme_set_chord_for_command (self, command, chord, phase); +} + +/** + * dzl_shortcut_theme_get_parent: + * @self: a #DzlShortcutTheme + * + * If the #DzlShortcutTheme:parent-name property has been set, this will fetch + * the parent #DzlShortcutTheme. + * + * Returns: (transfer none) (nullable): A #DzlShortcutTheme or %NULL. + */ +DzlShortcutTheme * +dzl_shortcut_theme_get_parent (DzlShortcutTheme *self) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_assert (DZL_IS_SHORTCUT_THEME (self)); + + if (g_strcmp0 (priv->name, "internal") == 0) + return NULL; + + if (priv->manager == NULL) + return NULL; + + if (priv->parent_name == NULL) + return _dzl_shortcut_manager_get_internal_theme (priv->manager); + + return dzl_shortcut_manager_get_theme_by_name (priv->manager, priv->parent_name); +} + +DzlShortcutMatch +_dzl_shortcut_theme_match (DzlShortcutTheme *self, + DzlShortcutPhase phase, + const DzlShortcutChord *chord, + DzlShortcutClosureChain **chain) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + DzlShortcutTheme *parent; + DzlShortcutMatch match1; + DzlShortcutMatch match2; + DzlShortcutMatch match3 = DZL_SHORTCUT_MATCH_NONE; + const gchar *action_id = NULL; + const gchar *command_id = NULL; + + g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (self), FALSE); + g_return_val_if_fail (chord != NULL, FALSE); + g_return_val_if_fail (chain != NULL, FALSE); + + match1 = dzl_shortcut_chord_table_lookup (priv->actions_table, chord, (gpointer *)&action_id); + + if (match1 == DZL_SHORTCUT_MATCH_EQUAL) + { + *chain = g_hash_table_lookup (priv->chains, action_id); + if ((*chain)->phase == phase) + return match1; + match1 = DZL_SHORTCUT_MATCH_NONE; + } + + match2 = dzl_shortcut_chord_table_lookup (priv->commands_table, chord, (gpointer *)&command_id); + + if (match2 == DZL_SHORTCUT_MATCH_EQUAL) + { + *chain = g_hash_table_lookup (priv->chains, command_id); + if ((*chain)->phase == phase) + return match2; + match2 = DZL_SHORTCUT_MATCH_NONE; + } + + /* + * We didn't find anything in this theme, try our parent theme. + */ + + parent = dzl_shortcut_theme_get_parent (self); + + if (parent != NULL) + { + match3 = _dzl_shortcut_theme_match (parent, phase, chord, chain); + + if (match3 == DZL_SHORTCUT_MATCH_EQUAL) + return match3; + } + + /* + * Nothing found, let the caller know if we found a partial match + * and ensure we zero out chain just to be safe. + */ + + *chain = NULL; + + return (match1 || match2 || match3) ? DZL_SHORTCUT_MATCH_PARTIAL : DZL_SHORTCUT_MATCH_NONE; +} + +void +_dzl_shortcut_theme_set_manager (DzlShortcutTheme *self, + DzlShortcutManager *manager) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + g_return_if_fail (!manager || DZL_IS_SHORTCUT_MANAGER (manager)); + g_return_if_fail (priv->manager == NULL || manager == NULL); + + priv->manager = manager; +} + +static void +copy_chord_to_table (const DzlShortcutChord *chord, + gpointer data, + gpointer user_data) +{ + DzlShortcutChordTable *dest = user_data; + const gchar *interned_string = data; + + g_assert (chord != NULL); + g_assert (data != NULL); + g_assert (dest != NULL); + + dzl_shortcut_chord_table_add (dest, chord, (gpointer)interned_string); +} + +void +_dzl_shortcut_theme_merge (DzlShortcutTheme *self, + DzlShortcutTheme *layer) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + DzlShortcutThemePrivate *layer_priv = dzl_shortcut_theme_get_instance_private (layer); + GHashTableIter hiter; + gpointer key; + gpointer value; + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + g_return_if_fail (DZL_IS_SHORTCUT_THEME (layer)); + g_return_if_fail (self != layer); + g_return_if_fail (DZL_IS_SHORTCUT_MANAGER (priv->manager)); + g_return_if_fail (DZL_IS_SHORTCUT_MANAGER (layer_priv->manager)); + g_return_if_fail (priv->manager == layer_priv->manager); + + /* + * This function will take the values in @layer and apply them to @self. + * Doing so will allow us to discard @layer afterwards. What this does for us + * is allow the base application and plugins all define aspects of a theme + * but have them merged into one. + * + * This function is destructive to @layer which is why it is private API and + * should only be used by the DzlShortcutManager. + */ + + if (priv->name == NULL && layer_priv->name != NULL) + priv->name = g_steal_pointer (&layer_priv->name); + + if (priv->title == NULL && layer_priv->title != NULL) + priv->title = g_steal_pointer (&layer_priv->title); + + if (priv->subtitle == NULL && layer_priv->subtitle != NULL) + priv->subtitle = g_steal_pointer (&layer_priv->subtitle); + + if (priv->parent_name == NULL && layer_priv->parent_name != NULL) + priv->parent_name = g_steal_pointer (&layer_priv->parent_name); + + /* + * Steal all of the closure chains from @layer and apply them to our + * overriden closure chains. + */ + + g_hash_table_iter_init (&hiter, layer_priv->chains); + while (g_hash_table_iter_next (&hiter, &key, &value)) + { + DzlShortcutClosureChain *chain = value; + const gchar *interned_key = key; + + g_hash_table_insert (priv->chains, (gpointer)interned_key, chain); + g_hash_table_iter_steal (&hiter); + } + + /* + * Merge all of the contexts found in the upper layer and apply them + * to our contexts. Since there could be additions/removals to the + * context, we can't just steal them, but have to merge their contents. + */ + g_hash_table_iter_init (&hiter, layer_priv->contexts); + while (g_hash_table_iter_next (&hiter, &key, &value)) + { + DzlShortcutContext *context = value; + DzlShortcutContext *base_context; + const gchar *interned_key = key; + + base_context = g_hash_table_lookup (priv->contexts, interned_key); + + /* + * If we do not contain this context yet, we can cheat and just steal the + * whole context rather than merge them. + */ + if (base_context == NULL) + { + g_hash_table_insert (priv->contexts, (gpointer)interned_key, value); + g_hash_table_iter_steal (&hiter); + continue; + } + + /* + * Okay, both layers have the context, so we need to merge them. + */ + _dzl_shortcut_context_merge (base_context, context); + } + + /* Merge any associated resources. */ + if (layer_priv->resource_providers != NULL) + { + GHashTableIter iter; + + if (priv->resource_providers == NULL) + priv->resource_providers = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref); + + g_hash_table_iter_init (&iter, layer_priv->resource_providers); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + g_hash_table_iter_steal (&iter); + g_hash_table_insert (priv->resource_providers, key, value); + } + } + + /* + * Copy our action and commands chords over. These are all const data, so no + * need to be tricky about stealing data or what data we are safe to + * copy/steal/ref/etc. + */ + dzl_shortcut_chord_table_foreach (layer_priv->actions_table, copy_chord_to_table, priv->actions_table); + dzl_shortcut_chord_table_foreach (layer_priv->commands_table, copy_chord_to_table, priv->commands_table); +} + +void +dzl_shortcut_theme_add_css_resource (DzlShortcutTheme *self, + const gchar *path) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + g_autoptr(GtkCssProvider) provider = NULL; + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + g_return_if_fail (path != NULL); + g_return_if_fail (*path == '/' || g_str_has_prefix (path, "resource://")); + + if (priv->resource_providers == NULL) + priv->resource_providers = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref); + + path = g_intern_string (path); + + provider = gtk_css_provider_new (); + + if (g_str_has_prefix (path, "resource://")) + { + const gchar *adjpath = path + strlen ("resource://"); + + gtk_css_provider_load_from_resource (provider, adjpath); + g_hash_table_insert (priv->resource_providers, (gpointer)path, g_steal_pointer (&provider)); + } + else + { + g_autoptr(GError) error = NULL; + + if (!gtk_css_provider_load_from_path (provider, path, &error)) + g_warning ("%s", error->message); + else + g_hash_table_insert (priv->resource_providers, (gpointer)path, g_steal_pointer (&provider)); + } +} + +void +dzl_shortcut_theme_remove_css_resource (DzlShortcutTheme *self, + const gchar *path) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + g_return_if_fail (path != NULL); + + if (priv->resource_providers != NULL) + g_hash_table_remove (priv->resource_providers, g_intern_string (path)); +} + +void +_dzl_shortcut_theme_attach (DzlShortcutTheme *self) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + + if (priv->resource_providers != NULL) + { + GdkScreen *screen = gdk_screen_get_default (); + GtkStyleProvider *provider; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, priv->resource_providers); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&provider)) + { + DZL_TRACE_MSG ("adding CSS provider %p", provider); + gtk_style_context_add_provider_for_screen (screen, + provider, + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + } +} + +void +_dzl_shortcut_theme_detach (DzlShortcutTheme *self) +{ + DzlShortcutThemePrivate *priv = dzl_shortcut_theme_get_instance_private (self); + + DZL_ENTRY; + + g_return_if_fail (DZL_IS_SHORTCUT_THEME (self)); + + if (priv->resource_providers != NULL) + { + GdkScreen *screen = gdk_screen_get_default (); + GtkStyleProvider *provider; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, priv->resource_providers); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&provider)) + { + DZL_TRACE_MSG ("removing CSS provider %p", provider); + gtk_style_context_remove_provider_for_screen (screen, provider); + } + } + + DZL_EXIT; +} diff --git a/src/shortcuts/dzl-shortcut-theme.h b/src/shortcuts/dzl-shortcut-theme.h new file mode 100644 index 0000000..d7689db --- /dev/null +++ b/src/shortcuts/dzl-shortcut-theme.h @@ -0,0 +1,144 @@ +/* dzl-shortcut-theme.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 2 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 . + */ + +#ifndef DZL_SHORTCUT_THEME_H +#define DZL_SHORTCUT_THEME_H + +#include + +#include "dzl-version-macros.h" + +#include "shortcuts/dzl-shortcut-chord.h" +#include "shortcuts/dzl-shortcut-context.h" +#include "shortcuts/dzl-shortcut-phase.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUT_THEME (dzl_shortcut_theme_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlShortcutTheme, dzl_shortcut_theme, DZL, SHORTCUT_THEME, GObject) + +struct _DzlShortcutThemeClass +{ + GObjectClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +DzlShortcutTheme *dzl_shortcut_theme_new (const gchar *name); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_shortcut_theme_get_name (DzlShortcutTheme *self); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_shortcut_theme_get_title (DzlShortcutTheme *self); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_shortcut_theme_get_subtitle (DzlShortcutTheme *self); +DZL_AVAILABLE_IN_ALL +DzlShortcutTheme *dzl_shortcut_theme_get_parent (DzlShortcutTheme *self); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_shortcut_theme_get_parent_name (DzlShortcutTheme *self); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_theme_set_parent_name (DzlShortcutTheme *self, + const gchar *parent_name); +DZL_AVAILABLE_IN_ALL +DzlShortcutContext *dzl_shortcut_theme_find_default_context (DzlShortcutTheme *self, + GtkWidget *widget); +DZL_AVAILABLE_IN_ALL +DzlShortcutContext *dzl_shortcut_theme_find_context_by_name (DzlShortcutTheme *self, + const gchar *name); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_theme_add_command (DzlShortcutTheme *self, + const gchar *accelerator, + const gchar *command); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_theme_add_context (DzlShortcutTheme *self, + DzlShortcutContext *context); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_theme_set_chord_for_action (DzlShortcutTheme *self, + const gchar *detailed_action_name, + const DzlShortcutChord *chord, + DzlShortcutPhase phase); +DZL_AVAILABLE_IN_ALL +const DzlShortcutChord *dzl_shortcut_theme_get_chord_for_action (DzlShortcutTheme *self, + const gchar *detailed_action_name); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_theme_set_accel_for_action (DzlShortcutTheme *self, + const gchar *detailed_action_name, + const gchar *accel, + DzlShortcutPhase phase); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_theme_set_chord_for_command (DzlShortcutTheme *self, + const gchar *command, + const DzlShortcutChord *chord, + DzlShortcutPhase phase); +DZL_AVAILABLE_IN_ALL +const DzlShortcutChord *dzl_shortcut_theme_get_chord_for_command (DzlShortcutTheme *self, + const gchar *command); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_theme_set_accel_for_command (DzlShortcutTheme *self, + const gchar *command, + const gchar *accel, + DzlShortcutPhase phase); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_theme_load_from_data (DzlShortcutTheme *self, + const gchar *data, + gssize len, + GError **error); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_theme_load_from_file (DzlShortcutTheme *self, + GFile *file, + GCancellable *cancellable, + GError **error); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_theme_load_from_path (DzlShortcutTheme *self, + const gchar *path, + GCancellable *cancellable, + GError **error); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_theme_save_to_file (DzlShortcutTheme *self, + GFile *file, + GCancellable *cancellable, + GError **error); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_theme_save_to_stream (DzlShortcutTheme *self, + GOutputStream *stream, + GCancellable *cancellable, + GError **error); +DZL_AVAILABLE_IN_ALL +gboolean dzl_shortcut_theme_save_to_path (DzlShortcutTheme *self, + const gchar *path, + GCancellable *cancellable, + GError **error); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_theme_add_css_resource (DzlShortcutTheme *self, + const gchar *path); +DZL_AVAILABLE_IN_ALL +void dzl_shortcut_theme_remove_css_resource (DzlShortcutTheme *self, + const gchar *path); + +G_END_DECLS + +#endif /* DZL_SHORTCUT_THEME_H */ diff --git a/src/shortcuts/dzl-shortcuts-group.c b/src/shortcuts/dzl-shortcuts-group.c new file mode 100644 index 0000000..dd4487f --- /dev/null +++ b/src/shortcuts/dzl-shortcuts-group.c @@ -0,0 +1,383 @@ +/* dzl-shortcuts-group.c + * + * Copyright (C) 2015 Christian Hergert + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#include + +#include "config.h" + +#include "shortcuts/dzl-shortcuts-group.h" +#include "shortcuts/dzl-shortcuts-shortcut.h" + +/** + * SECTION:dzl-shortcuts-group + * @Title: DzlShortcutsGroup + * @Short_description: Represents a group of shortcuts in a DzlShortcutsWindow + * + * A DzlShortcutsGroup represents a group of related keyboard shortcuts + * or gestures. The group has a title. It may optionally be associated with + * a view of the application, which can be used to show only relevant shortcuts + * depending on the application context. + * + * This widget is only meant to be used with #DzlShortcutsWindow. + */ + +struct _DzlShortcutsGroup +{ + GtkBox parent_instance; + + GtkLabel *title; + gchar *view; + guint height; + + GtkSizeGroup *accel_size_group; + GtkSizeGroup *title_size_group; +}; + +struct _DzlShortcutsGroupClass +{ + GtkBoxClass parent_class; +}; + +G_DEFINE_TYPE (DzlShortcutsGroup, dzl_shortcuts_group, GTK_TYPE_BOX) + +enum { + PROP_0, + PROP_TITLE, + PROP_VIEW, + PROP_ACCEL_SIZE_GROUP, + PROP_TITLE_SIZE_GROUP, + PROP_HEIGHT, + LAST_PROP +}; + +static GParamSpec *properties[LAST_PROP]; + +static void +dzl_shortcuts_group_apply_accel_size_group (DzlShortcutsGroup *group, + GtkWidget *child) +{ + if (DZL_IS_SHORTCUTS_SHORTCUT (child)) + g_object_set (child, "accel-size-group", group->accel_size_group, NULL); +} + +static void +dzl_shortcuts_group_apply_title_size_group (DzlShortcutsGroup *group, + GtkWidget *child) +{ + if (DZL_IS_SHORTCUTS_SHORTCUT (child)) + g_object_set (child, "title-size-group", group->title_size_group, NULL); +} + +static void +dzl_shortcuts_group_set_accel_size_group (DzlShortcutsGroup *group, + GtkSizeGroup *size_group) +{ + GList *children, *l; + + g_set_object (&group->accel_size_group, size_group); + + children = gtk_container_get_children (GTK_CONTAINER (group)); + for (l = children; l; l = l->next) + dzl_shortcuts_group_apply_accel_size_group (group, GTK_WIDGET (l->data)); + g_list_free (children); +} + +static void +dzl_shortcuts_group_set_title_size_group (DzlShortcutsGroup *group, + GtkSizeGroup *size_group) +{ + GList *children, *l; + + g_set_object (&group->title_size_group, size_group); + + children = gtk_container_get_children (GTK_CONTAINER (group)); + for (l = children; l; l = l->next) + dzl_shortcuts_group_apply_title_size_group (group, GTK_WIDGET (l->data)); + g_list_free (children); +} + +static guint +dzl_shortcuts_group_get_height (DzlShortcutsGroup *group) +{ + GList *children, *l; + guint height; + + height = 1; + + children = gtk_container_get_children (GTK_CONTAINER (group)); + for (l = children; l; l = l->next) + { + GtkWidget *child = l->data; + + if (!gtk_widget_get_visible (child)) + continue; + else if (DZL_IS_SHORTCUTS_SHORTCUT (child)) + height += 1; + } + g_list_free (children); + + return height; +} + +static void +dzl_shortcuts_group_add (GtkContainer *container, + GtkWidget *widget) +{ + if (DZL_IS_SHORTCUTS_SHORTCUT (widget)) + { + GTK_CONTAINER_CLASS (dzl_shortcuts_group_parent_class)->add (container, widget); + dzl_shortcuts_group_apply_accel_size_group (DZL_SHORTCUTS_GROUP (container), widget); + dzl_shortcuts_group_apply_title_size_group (DZL_SHORTCUTS_GROUP (container), widget); + } + else + g_warning ("Can't add children of type %s to %s", + G_OBJECT_TYPE_NAME (widget), + G_OBJECT_TYPE_NAME (container)); +} + +typedef struct { + GtkCallback callback; + gpointer data; + gboolean include_internal; +} CallbackData; + +static void +forall_cb (GtkWidget *widget, gpointer data) +{ + DzlShortcutsGroup *self; + CallbackData *cbdata = data; + + self = DZL_SHORTCUTS_GROUP (gtk_widget_get_parent (widget)); + if (cbdata->include_internal || widget != (GtkWidget*)self->title) + cbdata->callback (widget, cbdata->data); +} + +static void +dzl_shortcuts_group_forall (GtkContainer *container, + gboolean include_internal, + GtkCallback callback, + gpointer callback_data) +{ + CallbackData cbdata; + + cbdata.include_internal = include_internal; + cbdata.callback = callback; + cbdata.data = callback_data; + + GTK_CONTAINER_CLASS (dzl_shortcuts_group_parent_class)->forall (container, include_internal, forall_cb, &cbdata); +} + +static void +dzl_shortcuts_group_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutsGroup *self = DZL_SHORTCUTS_GROUP (object); + + switch (prop_id) + { + case PROP_TITLE: + g_value_set_string (value, gtk_label_get_label (self->title)); + break; + + case PROP_VIEW: + g_value_set_string (value, self->view); + break; + + case PROP_HEIGHT: + g_value_set_uint (value, dzl_shortcuts_group_get_height (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcuts_group_direction_changed (GtkWidget *widget, + GtkTextDirection previous_dir) +{ + GTK_WIDGET_CLASS (dzl_shortcuts_group_parent_class)->direction_changed (widget, previous_dir); + g_object_notify (G_OBJECT (widget), "height"); +} + +static void +dzl_shortcuts_group_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutsGroup *self = DZL_SHORTCUTS_GROUP (object); + + switch (prop_id) + { + case PROP_TITLE: + gtk_label_set_label (self->title, g_value_get_string (value)); + break; + + case PROP_VIEW: + g_free (self->view); + self->view = g_value_dup_string (value); + break; + + case PROP_ACCEL_SIZE_GROUP: + dzl_shortcuts_group_set_accel_size_group (self, GTK_SIZE_GROUP (g_value_get_object (value))); + break; + + case PROP_TITLE_SIZE_GROUP: + dzl_shortcuts_group_set_title_size_group (self, GTK_SIZE_GROUP (g_value_get_object (value))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcuts_group_finalize (GObject *object) +{ + DzlShortcutsGroup *self = DZL_SHORTCUTS_GROUP (object); + + g_free (self->view); + g_set_object (&self->accel_size_group, NULL); + g_set_object (&self->title_size_group, NULL); + + G_OBJECT_CLASS (dzl_shortcuts_group_parent_class)->finalize (object); +} + +static void +dzl_shortcuts_group_dispose (GObject *object) +{ + DzlShortcutsGroup *self = DZL_SHORTCUTS_GROUP (object); + + /* + * Since we overload forall(), the inherited destroy() won't work as normal. + * Remove internal widgets ourself. + */ + if (self->title) + { + gtk_widget_destroy (GTK_WIDGET (self->title)); + self->title = NULL; + } + + G_OBJECT_CLASS (dzl_shortcuts_group_parent_class)->dispose (object); +} + +static void +dzl_shortcuts_group_class_init (DzlShortcutsGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->finalize = dzl_shortcuts_group_finalize; + object_class->get_property = dzl_shortcuts_group_get_property; + object_class->set_property = dzl_shortcuts_group_set_property; + object_class->dispose = dzl_shortcuts_group_dispose; + + widget_class->direction_changed = dzl_shortcuts_group_direction_changed; + container_class->add = dzl_shortcuts_group_add; + container_class->forall = dzl_shortcuts_group_forall; + + /** + * DzlShortcutsGroup:title: + * + * The title for this group of shortcuts. + */ + properties[PROP_TITLE] = + g_param_spec_string ("title", _("Title"), _("Title"), + "", + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsGroup:view: + * + * An optional view that the shortcuts in this group are relevant for. + * The group will be hidden if the #DzlShortcutsWindow:view-name property + * does not match the view of this group. + * + * Set this to %NULL to make the group always visible. + */ + properties[PROP_VIEW] = + g_param_spec_string ("view", _("View"), _("View"), + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsGroup:accel-size-group: + * + * The size group for the accelerator portion of shortcuts in this group. + * + * This is used internally by GTK+, and must not be modified by applications. + */ + properties[PROP_ACCEL_SIZE_GROUP] = + g_param_spec_object ("accel-size-group", + _("Accelerator Size Group"), + _("Accelerator Size Group"), + GTK_TYPE_SIZE_GROUP, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsGroup:title-size-group: + * + * The size group for the textual portion of shortcuts in this group. + * + * This is used internally by GTK+, and must not be modified by applications. + */ + properties[PROP_TITLE_SIZE_GROUP] = + g_param_spec_object ("title-size-group", + _("Title Size Group"), + _("Title Size Group"), + GTK_TYPE_SIZE_GROUP, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsGroup:height: + * + * A rough measure for the number of lines in this group. + * + * This is used internally by GTK+, and is not useful for applications. + */ + properties[PROP_HEIGHT] = + g_param_spec_uint ("height", _("Height"), _("Height"), + 0, G_MAXUINT, 1, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_shortcuts_group_init (DzlShortcutsGroup *self) +{ + PangoAttrList *attrs; + + gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_VERTICAL); + gtk_box_set_spacing (GTK_BOX (self), 10); + + attrs = pango_attr_list_new (); + pango_attr_list_insert (attrs, pango_attr_weight_new (PANGO_WEIGHT_BOLD)); + self->title = g_object_new (GTK_TYPE_LABEL, + "attributes", attrs, + "visible", TRUE, + "xalign", 0.0f, + NULL); + pango_attr_list_unref (attrs); + + GTK_CONTAINER_CLASS (dzl_shortcuts_group_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET (self->title)); +} diff --git a/src/shortcuts/dzl-shortcuts-group.h b/src/shortcuts/dzl-shortcuts-group.h new file mode 100644 index 0000000..c42432e --- /dev/null +++ b/src/shortcuts/dzl-shortcuts-group.h @@ -0,0 +1,44 @@ +/* dzl-shortcuts-groupprivate.h + * + * Copyright (C) 2015 Christian Hergert + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#ifndef __DZL_SHORTCUTS_GROUP_H__ +#define __DZL_SHORTCUTS_GROUP_H__ + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUTS_GROUP (dzl_shortcuts_group_get_type ()) +#define DZL_SHORTCUTS_GROUP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), DZL_TYPE_SHORTCUTS_GROUP, DzlShortcutsGroup)) +#define DZL_SHORTCUTS_GROUP_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), DZL_TYPE_SHORTCUTS_GROUP, DzlShortcutsGroupClass)) +#define DZL_IS_SHORTCUTS_GROUP(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), DZL_TYPE_SHORTCUTS_GROUP)) +#define DZL_IS_SHORTCUTS_GROUP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), DZL_TYPE_SHORTCUTS_GROUP)) +#define DZL_SHORTCUTS_GROUP_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), DZL_TYPE_SHORTCUTS_GROUP, DzlShortcutsGroupClass)) + + +typedef struct _DzlShortcutsGroup DzlShortcutsGroup; +typedef struct _DzlShortcutsGroupClass DzlShortcutsGroupClass; + +DZL_AVAILABLE_IN_ALL +GType dzl_shortcuts_group_get_type (void) G_GNUC_CONST; + +G_END_DECLS + +#endif /* __DZL_SHORTCUTS_GROUP_H__ */ diff --git a/src/shortcuts/dzl-shortcuts-section.c b/src/shortcuts/dzl-shortcuts-section.c new file mode 100644 index 0000000..3ee3f5d --- /dev/null +++ b/src/shortcuts/dzl-shortcuts-section.c @@ -0,0 +1,819 @@ +/* dzl-shortcuts-section.c + * + * Copyright (C) 2015 Christian Hergert + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#include + +#include "config.h" + +#include "shortcuts/dzl-shortcuts-group.h" +#include "shortcuts/dzl-shortcuts-section.h" + +/** + * SECTION:dzl-shortcuts-section + * @Title: DzlShortcutsSection + * @Short_description: Represents an application mode in a DzlShortcutsWindow + * + * A DzlShortcutsSection collects all the keyboard shortcuts and gestures + * for a major application mode. If your application needs multiple sections, + * you should give each section a unique #DzlShortcutsSection:section-name and + * a #DzlShortcutsSection:title that can be shown in the section selector of + * the DzlShortcutsWindow. + * + * The #DzlShortcutsSection:max-height property can be used to influence how + * the groups in the section are distributed over pages and columns. + * + * This widget is only meant to be used with #DzlShortcutsWindow. + */ + +struct _DzlShortcutsSection +{ + GtkBox parent_instance; + + gchar *name; + gchar *title; + gchar *view_name; + guint max_height; + + GtkStack *stack; + GtkStackSwitcher *switcher; + GtkWidget *show_all; + GtkWidget *footer; + GList *groups; + + gboolean has_filtered_group; + gboolean need_reflow; + + GtkGesture *pan_gesture; +}; + +struct _DzlShortcutsSectionClass +{ + GtkBoxClass parent_class; + + gboolean (* change_current_page) (DzlShortcutsSection *self, + gint offset); + +}; + +G_DEFINE_TYPE (DzlShortcutsSection, dzl_shortcuts_section, GTK_TYPE_BOX) + +enum { + PROP_0, + PROP_TITLE, + PROP_SECTION_NAME, + PROP_VIEW_NAME, + PROP_MAX_HEIGHT, + LAST_PROP +}; + +enum { + CHANGE_CURRENT_PAGE, + LAST_SIGNAL +}; + +static GParamSpec *properties[LAST_PROP]; +static guint signals[LAST_SIGNAL]; + +static void dzl_shortcuts_section_set_view_name (DzlShortcutsSection *self, + const gchar *view_name); +static void dzl_shortcuts_section_set_max_height (DzlShortcutsSection *self, + guint max_height); +static void dzl_shortcuts_section_add_group (DzlShortcutsSection *self, + DzlShortcutsGroup *group); + +static void dzl_shortcuts_section_show_all (DzlShortcutsSection *self); +static void dzl_shortcuts_section_filter_groups (DzlShortcutsSection *self); +static void dzl_shortcuts_section_reflow_groups (DzlShortcutsSection *self); +static void dzl_shortcuts_section_maybe_reflow (DzlShortcutsSection *self); + +static gboolean dzl_shortcuts_section_change_current_page (DzlShortcutsSection *self, + gint offset); + +static void dzl_shortcuts_section_pan_gesture_pan (GtkGesturePan *gesture, + GtkPanDirection direction, + gdouble offset, + DzlShortcutsSection *self); + +static void +dzl_shortcuts_section_add (GtkContainer *container, + GtkWidget *child) +{ + DzlShortcutsSection *self = DZL_SHORTCUTS_SECTION (container); + + if (DZL_IS_SHORTCUTS_GROUP (child)) + dzl_shortcuts_section_add_group (self, DZL_SHORTCUTS_GROUP (child)); + else + g_warning ("Can't add children of type %s to %s", + G_OBJECT_TYPE_NAME (child), + G_OBJECT_TYPE_NAME (container)); +} + +static void +dzl_shortcuts_section_remove (GtkContainer *container, + GtkWidget *child) +{ + DzlShortcutsSection *self = (DzlShortcutsSection *)container; + + if (DZL_IS_SHORTCUTS_GROUP (child) && + gtk_widget_is_ancestor (child, GTK_WIDGET (container))) + { + self->groups = g_list_remove (self->groups, child); + gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (child)), child); + } + else + GTK_CONTAINER_CLASS (dzl_shortcuts_section_parent_class)->remove (container, child); +} + +static void +dzl_shortcuts_section_forall (GtkContainer *container, + gboolean include_internal, + GtkCallback callback, + gpointer callback_data) +{ + DzlShortcutsSection *self = (DzlShortcutsSection *)container; + GList *l; + + if (include_internal) + { + GTK_CONTAINER_CLASS (dzl_shortcuts_section_parent_class)->forall (container, include_internal, callback, callback_data); + } + else + { + for (l = self->groups; l; l = l->next) + { + GtkWidget *group = l->data; + callback (group, callback_data); + } + } +} + +static void +map_child (GtkWidget *child) +{ + if (gtk_widget_get_visible (child) && + gtk_widget_get_child_visible (child) && + !gtk_widget_get_mapped (child)) + gtk_widget_map (child); +} + +static void +dzl_shortcuts_section_map (GtkWidget *widget) +{ + DzlShortcutsSection *self = DZL_SHORTCUTS_SECTION (widget); + + if (self->need_reflow) + dzl_shortcuts_section_reflow_groups (self); + + gtk_widget_set_mapped (widget, TRUE); + + map_child (GTK_WIDGET (self->stack)); + map_child (GTK_WIDGET (self->footer)); +} + +static void +dzl_shortcuts_section_unmap (GtkWidget *widget) +{ + DzlShortcutsSection *self = DZL_SHORTCUTS_SECTION (widget); + + gtk_widget_set_mapped (widget, FALSE); + + gtk_widget_unmap (GTK_WIDGET (self->footer)); + gtk_widget_unmap (GTK_WIDGET (self->stack)); +} + +static void +dzl_shortcuts_section_destroy (GtkWidget *widget) +{ + DzlShortcutsSection *self = DZL_SHORTCUTS_SECTION (widget); + + if (self->stack) + { + gtk_widget_destroy (GTK_WIDGET (self->stack)); + self->stack = NULL; + } + + if (self->footer) + { + gtk_widget_destroy (GTK_WIDGET (self->footer)); + self->footer = NULL; + } + + g_list_free (self->groups); + self->groups = NULL; + + GTK_WIDGET_CLASS (dzl_shortcuts_section_parent_class)->destroy (widget); +} + +static void +dzl_shortcuts_section_finalize (GObject *object) +{ + DzlShortcutsSection *self = (DzlShortcutsSection *)object; + + g_clear_pointer (&self->name, g_free); + g_clear_pointer (&self->title, g_free); + g_clear_pointer (&self->view_name, g_free); + g_clear_object (&self->pan_gesture); + + G_OBJECT_CLASS (dzl_shortcuts_section_parent_class)->finalize (object); +} + +static void +dzl_shortcuts_section_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutsSection *self = (DzlShortcutsSection *)object; + + switch (prop_id) + { + case PROP_SECTION_NAME: + g_value_set_string (value, self->name); + break; + + case PROP_VIEW_NAME: + g_value_set_string (value, self->view_name); + break; + + case PROP_TITLE: + g_value_set_string (value, self->title); + break; + + case PROP_MAX_HEIGHT: + g_value_set_uint (value, self->max_height); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcuts_section_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutsSection *self = (DzlShortcutsSection *)object; + + switch (prop_id) + { + case PROP_SECTION_NAME: + g_free (self->name); + self->name = g_value_dup_string (value); + break; + + case PROP_VIEW_NAME: + dzl_shortcuts_section_set_view_name (self, g_value_get_string (value)); + break; + + case PROP_TITLE: + g_free (self->title); + self->title = g_value_dup_string (value); + break; + + case PROP_MAX_HEIGHT: + dzl_shortcuts_section_set_max_height (self, g_value_get_uint (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static GType +dzl_shortcuts_section_child_type (GtkContainer *container) +{ + return GTK_TYPE_SHORTCUTS_GROUP; +} + +static void +dzl_shortcuts_section_class_init (DzlShortcutsSectionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + GtkBindingSet *binding_set; + + object_class->finalize = dzl_shortcuts_section_finalize; + object_class->get_property = dzl_shortcuts_section_get_property; + object_class->set_property = dzl_shortcuts_section_set_property; + + widget_class->map = dzl_shortcuts_section_map; + widget_class->unmap = dzl_shortcuts_section_unmap; + widget_class->destroy = dzl_shortcuts_section_destroy; + + container_class->add = dzl_shortcuts_section_add; + container_class->remove = dzl_shortcuts_section_remove; + container_class->forall = dzl_shortcuts_section_forall; + container_class->child_type = dzl_shortcuts_section_child_type; + + klass->change_current_page = dzl_shortcuts_section_change_current_page; + + /** + * DzlShortcutsSection:section-name: + * + * A unique name to identify this section among the sections + * added to the DzlShortcutsWindow. Setting the #DzlShortcutsWindow:section-name + * property to this string will make this section shown in the + * DzlShortcutsWindow. + */ + properties[PROP_SECTION_NAME] = + g_param_spec_string ("section-name", _("Section Name"), _("Section Name"), + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsSection:view-name: + * + * A view name to filter the groups in this section by. + * See #DzlShortcutsGroup:view. + * + * Applications are expected to use the #DzlShortcutsWindow:view-name + * property for this purpose. + */ + properties[PROP_VIEW_NAME] = + g_param_spec_string ("view-name", _("View Name"), _("View Name"), + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY)); + + /** + * DzlShortcutsSection:title: + * + * The string to show in the section selector of the DzlShortcutsWindow + * for this section. If there is only one section, you don't need to + * set a title, since the section selector will not be shown in this case. + */ + properties[PROP_TITLE] = + g_param_spec_string ("title", _("Title"), _("Title"), + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsSection:max-height: + * + * The maximum number of lines to allow per column. This property can + * be used to influence how the groups in this section are distributed + * across pages and columns. The default value of 15 should work in + * for most cases. + */ + properties[PROP_MAX_HEIGHT] = + g_param_spec_uint ("max-height", _("Maximum Height"), _("Maximum Height"), + 0, G_MAXUINT, 15, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + signals[CHANGE_CURRENT_PAGE] = + g_signal_new (_("change-current-page"), + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (DzlShortcutsSectionClass, change_current_page), + NULL, NULL, NULL, + G_TYPE_BOOLEAN, 1, G_TYPE_INT); + + binding_set = gtk_binding_set_by_class (klass); + gtk_binding_entry_add_signal (binding_set, + GDK_KEY_Page_Up, 0, + "change-current-page", 1, + G_TYPE_INT, -1); + gtk_binding_entry_add_signal (binding_set, + GDK_KEY_Page_Down, 0, + "change-current-page", 1, + G_TYPE_INT, 1); + gtk_binding_entry_add_signal (binding_set, + GDK_KEY_Page_Up, GDK_CONTROL_MASK, + "change-current-page", 1, + G_TYPE_INT, -1); + gtk_binding_entry_add_signal (binding_set, + GDK_KEY_Page_Down, GDK_CONTROL_MASK, + "change-current-page", 1, + G_TYPE_INT, 1); +} + +static void +dzl_shortcuts_section_init (DzlShortcutsSection *self) +{ + self->max_height = 15; + + gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_VERTICAL); + gtk_box_set_homogeneous (GTK_BOX (self), FALSE); + gtk_box_set_spacing (GTK_BOX (self), 22); + gtk_container_set_border_width (GTK_CONTAINER (self), 24); + + self->stack = g_object_new (GTK_TYPE_STACK, + "homogeneous", TRUE, + "transition-type", GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT, + "vexpand", TRUE, + "visible", TRUE, + NULL); + GTK_CONTAINER_CLASS (dzl_shortcuts_section_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET (self->stack)); + + self->switcher = g_object_new (GTK_TYPE_STACK_SWITCHER, + "halign", GTK_ALIGN_CENTER, + "stack", self->stack, + "spacing", 12, + "no-show-all", TRUE, + NULL); + + gtk_style_context_remove_class (gtk_widget_get_style_context (GTK_WIDGET (self->switcher)), GTK_STYLE_CLASS_LINKED); + + self->show_all = gtk_button_new_with_mnemonic (_("_Show All")); + gtk_widget_set_no_show_all (self->show_all, TRUE); + g_signal_connect_swapped (self->show_all, "clicked", + G_CALLBACK (dzl_shortcuts_section_show_all), self); + + self->footer = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 20); + GTK_CONTAINER_CLASS (dzl_shortcuts_section_parent_class)->add (GTK_CONTAINER (self), self->footer); + + gtk_box_set_center_widget (GTK_BOX (self->footer), GTK_WIDGET (self->switcher)); + gtk_box_pack_end (GTK_BOX (self->footer), self->show_all, TRUE, TRUE, 0); + gtk_widget_set_halign (self->show_all, GTK_ALIGN_END); + + self->pan_gesture = gtk_gesture_pan_new (GTK_WIDGET (self->stack), GTK_ORIENTATION_HORIZONTAL); + g_signal_connect (self->pan_gesture, "pan", + G_CALLBACK (dzl_shortcuts_section_pan_gesture_pan), self); +} + +static void +dzl_shortcuts_section_set_view_name (DzlShortcutsSection *self, + const gchar *view_name) +{ + if (g_strcmp0 (self->view_name, view_name) == 0) + return; + + g_free (self->view_name); + self->view_name = g_strdup (view_name); + + dzl_shortcuts_section_filter_groups (self); + dzl_shortcuts_section_reflow_groups (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VIEW_NAME]); +} + +static void +dzl_shortcuts_section_set_max_height (DzlShortcutsSection *self, + guint max_height) +{ + if (self->max_height == max_height) + return; + + self->max_height = max_height; + + dzl_shortcuts_section_maybe_reflow (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MAX_HEIGHT]); +} + +static void +dzl_shortcuts_section_add_group (DzlShortcutsSection *self, + DzlShortcutsGroup *group) +{ + GList *children; + GtkWidget *page, *column; + + children = gtk_container_get_children (GTK_CONTAINER (self->stack)); + if (children) + page = g_list_last (children)->data; + else + { + page = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 22); + gtk_stack_add_named (self->stack, page, "1"); + } + g_list_free (children); + + children = gtk_container_get_children (GTK_CONTAINER (page)); + if (children) + column = g_list_last (children)->data; + else + { + column = gtk_box_new (GTK_ORIENTATION_VERTICAL, 22); + gtk_container_add (GTK_CONTAINER (page), column); + } + g_list_free (children); + + gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (group)); + self->groups = g_list_append (self->groups, group); + + dzl_shortcuts_section_maybe_reflow (self); +} + +static void +dzl_shortcuts_section_show_all (DzlShortcutsSection *self) +{ + dzl_shortcuts_section_set_view_name (self, NULL); +} + +static void +update_group_visibility (GtkWidget *child, gpointer data) +{ + DzlShortcutsSection *self = data; + + if (DZL_IS_SHORTCUTS_GROUP (child)) + { + gchar *view; + gboolean match; + + g_object_get (child, "view", &view, NULL); + match = view == NULL || + self->view_name == NULL || + strcmp (view, self->view_name) == 0; + + gtk_widget_set_visible (child, match); + self->has_filtered_group |= !match; + + g_free (view); + } + else if (GTK_IS_CONTAINER (child)) + { + gtk_container_foreach (GTK_CONTAINER (child), update_group_visibility, data); + } +} + +static void +dzl_shortcuts_section_filter_groups (DzlShortcutsSection *self) +{ + self->has_filtered_group = FALSE; + + gtk_container_foreach (GTK_CONTAINER (self), update_group_visibility, self); + + gtk_widget_set_visible (GTK_WIDGET (self->show_all), self->has_filtered_group); + gtk_widget_set_visible (gtk_widget_get_parent (GTK_WIDGET (self->show_all)), + gtk_widget_get_visible (GTK_WIDGET (self->show_all)) || + gtk_widget_get_visible (GTK_WIDGET (self->switcher))); +} + +static void +dzl_shortcuts_section_maybe_reflow (DzlShortcutsSection *self) +{ + if (gtk_widget_get_mapped (GTK_WIDGET (self))) + dzl_shortcuts_section_reflow_groups (self); + else + self->need_reflow = TRUE; +} + +static void +adjust_page_buttons (GtkWidget *widget, + gpointer data) +{ + GtkWidget *label; + + gtk_style_context_add_class (gtk_widget_get_style_context (widget), "circular"); + + label = gtk_bin_get_child (GTK_BIN (widget)); + gtk_label_set_use_underline (GTK_LABEL (label), TRUE); +} + +static void +dzl_shortcuts_section_reflow_groups (DzlShortcutsSection *self) +{ + GList *pages, *p; + GList *columns, *c; + GList *groups, *g; + GList *children; + guint n_rows; + guint n_columns; + guint n_pages; + GtkWidget *current_page, *current_column; + + /* collect all groups from the current pages */ + groups = NULL; + pages = gtk_container_get_children (GTK_CONTAINER (self->stack)); + for (p = pages; p; p = p->next) + { + columns = gtk_container_get_children (GTK_CONTAINER (p->data)); + for (c = columns; c; c = c->next) + { + children = gtk_container_get_children (GTK_CONTAINER (c->data)); + groups = g_list_concat (groups, children); + } + g_list_free (columns); + } + g_list_free (pages); + + /* create new pages */ + current_page = NULL; + current_column = NULL; + pages = NULL; + n_rows = 0; + n_columns = 0; + n_pages = 0; + for (g = groups; g; g = g->next) + { + DzlShortcutsGroup *group = g->data; + guint height; + gboolean visible; + + g_object_get (group, + "visible", &visible, + "height", &height, + NULL); + if (!visible) + height = 0; + + if (current_column == NULL || n_rows + height > self->max_height) + { + GtkWidget *column; + GtkSizeGroup *size_group; + + column = gtk_box_new (GTK_ORIENTATION_VERTICAL, 22); + gtk_widget_show (column); + + size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); +G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gtk_size_group_set_ignore_hidden (size_group, TRUE); +G_GNUC_END_IGNORE_DEPRECATIONS + g_object_set_data_full (G_OBJECT (column), "accel-size-group", size_group, g_object_unref); + + size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); +G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gtk_size_group_set_ignore_hidden (size_group, TRUE); +G_GNUC_END_IGNORE_DEPRECATIONS + g_object_set_data_full (G_OBJECT (column), "title-size-group", size_group, g_object_unref); + + if (n_columns % 2 == 0) + { + GtkWidget *page; + + page = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 22); + gtk_widget_show (page); + + pages = g_list_append (pages, page); + current_page = page; + } + + gtk_container_add (GTK_CONTAINER (current_page), column); + current_column = column; + n_columns += 1; + n_rows = 0; + } + + n_rows += height; + + g_object_set (group, + "accel-size-group", g_object_get_data (G_OBJECT (current_column), "accel-size-group"), + "title-size-group", g_object_get_data (G_OBJECT (current_column), "title-size-group"), + NULL); + + g_object_ref (group); + gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET (group))), GTK_WIDGET (group)); + gtk_container_add (GTK_CONTAINER (current_column), GTK_WIDGET (group)); + g_object_unref (group); + } + + /* balance the last page */ + if (n_columns % 2 == 1) + { + GtkWidget *column; + GtkSizeGroup *size_group; + GList *content; + guint n; + + column = gtk_box_new (GTK_ORIENTATION_VERTICAL, 22); + gtk_widget_show (column); + + size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); +G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gtk_size_group_set_ignore_hidden (size_group, TRUE); +G_GNUC_END_IGNORE_DEPRECATIONS + g_object_set_data_full (G_OBJECT (column), "accel-size-group", size_group, g_object_unref); + size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); +G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gtk_size_group_set_ignore_hidden (size_group, TRUE); +G_GNUC_END_IGNORE_DEPRECATIONS + g_object_set_data_full (G_OBJECT (column), "title-size-group", size_group, g_object_unref); + + gtk_container_add (GTK_CONTAINER (current_page), column); + + content = gtk_container_get_children (GTK_CONTAINER (current_column)); + n = 0; + + for (g = g_list_last (content); g; g = g->prev) + { + DzlShortcutsGroup *group = g->data; + guint height; + gboolean visible; + + g_object_get (group, + "visible", &visible, + "height", &height, + NULL); + if (!visible) + height = 0; + + if (n_rows - height == 0) + break; + if (ABS ((gint)n_rows - (gint)n) < ABS (((gint)n_rows - (gint)height) - ((gint)n + (gint)height))) + break; + + n_rows -= height; + n += height; + } + + for (g = g->next; g; g = g->next) + { + DzlShortcutsGroup *group = g->data; + + g_object_set (group, + "accel-size-group", g_object_get_data (G_OBJECT (column), "accel-size-group"), + "title-size-group", g_object_get_data (G_OBJECT (column), "title-size-group"), + NULL); + + g_object_ref (group); + gtk_container_remove (GTK_CONTAINER (current_column), GTK_WIDGET (group)); + gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (group)); + g_object_unref (group); + } + + g_list_free (content); + } + + /* replace the current pages with the new pages */ + children = gtk_container_get_children (GTK_CONTAINER (self->stack)); + g_list_free_full (children, (GDestroyNotify)gtk_widget_destroy); + + for (p = pages, n_pages = 0; p; p = p->next, n_pages++) + { + GtkWidget *page = p->data; + gchar *title; + + title = g_strdup_printf ("_%u", n_pages + 1); + gtk_stack_add_titled (self->stack, page, title, title); + g_free (title); + } + + /* fix up stack switcher */ + gtk_container_foreach (GTK_CONTAINER (self->switcher), adjust_page_buttons, NULL); + gtk_widget_set_visible (GTK_WIDGET (self->switcher), (n_pages > 1)); + gtk_widget_set_visible (gtk_widget_get_parent (GTK_WIDGET (self->switcher)), + gtk_widget_get_visible (GTK_WIDGET (self->show_all)) || + gtk_widget_get_visible (GTK_WIDGET (self->switcher))); + + /* clean up */ + g_list_free (groups); + g_list_free (pages); + + self->need_reflow = FALSE; +} + +static gboolean +dzl_shortcuts_section_change_current_page (DzlShortcutsSection *self, + gint offset) +{ + GtkWidget *child; + GList *children, *l; + + child = gtk_stack_get_visible_child (self->stack); + children = gtk_container_get_children (GTK_CONTAINER (self->stack)); + l = g_list_find (children, child); + + if (offset == 1) + l = l->next; + else if (offset == -1) + l = l->prev; + else + g_assert_not_reached (); + + if (l) + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (l->data)); + else + gtk_widget_error_bell (GTK_WIDGET (self)); + + g_list_free (children); + + return TRUE; +} + +static void +dzl_shortcuts_section_pan_gesture_pan (GtkGesturePan *gesture, + GtkPanDirection direction, + gdouble offset, + DzlShortcutsSection *self) +{ + if (offset < 50) + return; + + if (direction == GTK_PAN_DIRECTION_LEFT) + dzl_shortcuts_section_change_current_page (self, 1); + else if (direction == GTK_PAN_DIRECTION_RIGHT) + dzl_shortcuts_section_change_current_page (self, -1); + else + g_assert_not_reached (); + + gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED); +} diff --git a/src/shortcuts/dzl-shortcuts-section.h b/src/shortcuts/dzl-shortcuts-section.h new file mode 100644 index 0000000..7a9378a --- /dev/null +++ b/src/shortcuts/dzl-shortcuts-section.h @@ -0,0 +1,43 @@ +/* dzl-shortcuts-section.h + * + * Copyright (C) 2015 Christian Hergert + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#ifndef __DZL_SHORTCUTS_SECTION_H__ +#define __DZL_SHORTCUTS_SECTION_H__ + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUTS_SECTION (dzl_shortcuts_section_get_type ()) +#define DZL_SHORTCUTS_SECTION(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), DZL_TYPE_SHORTCUTS_SECTION, DzlShortcutsSection)) +#define DZL_SHORTCUTS_SECTION_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), DZL_TYPE_SHORTCUTS_SECTION, DzlShortcutsSectionClass)) +#define DZL_IS_SHORTCUTS_SECTION(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), DZL_TYPE_SHORTCUTS_SECTION)) +#define DZL_IS_SHORTCUTS_SECTION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), DZL_TYPE_SHORTCUTS_SECTION)) +#define DZL_SHORTCUTS_SECTION_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), DZL_TYPE_SHORTCUTS_SECTION, DzlShortcutsSectionClass)) + +typedef struct _DzlShortcutsSection DzlShortcutsSection; +typedef struct _DzlShortcutsSectionClass DzlShortcutsSectionClass; + +DZL_AVAILABLE_IN_ALL +GType dzl_shortcuts_section_get_type (void) G_GNUC_CONST; + +G_END_DECLS + +#endif /* __DZL_SHORTCUTS_SECTION_H__ */ diff --git a/src/shortcuts/dzl-shortcuts-shortcut-private.h b/src/shortcuts/dzl-shortcuts-shortcut-private.h new file mode 100644 index 0000000..b94cd93 --- /dev/null +++ b/src/shortcuts/dzl-shortcuts-shortcut-private.h @@ -0,0 +1,37 @@ +/* GTK - The GIMP Toolkit + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +/* + * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS + * file for a list of people on the GTK+ Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GTK+ at ftp://ftp.gtk.org/pub/gtk/. + */ + +#ifndef __DZL_SHORTCUTS_SHORTCUT_PRIVATE_H__ +#define __DZL_SHORTCUTS_SHORTCUT_PRIVATE_H__ + +#include "dzl-shortcuts-shortcut.h" + +G_BEGIN_DECLS + +void dzl_shortcuts_shortcut_update_accel (DzlShortcutsShortcut *self, + GtkWindow *window); + +G_END_DECLS + +#endif /* __GTK_sHORTCUTS_SHORTCUT_PRIVATE_H__ */ diff --git a/src/shortcuts/dzl-shortcuts-shortcut.c b/src/shortcuts/dzl-shortcuts-shortcut.c new file mode 100644 index 0000000..a78771a --- /dev/null +++ b/src/shortcuts/dzl-shortcuts-shortcut.c @@ -0,0 +1,732 @@ +/* dzl-shortcuts-shortcut.c + * + * Copyright (C) 2015 Christian Hergert + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#include + +#include "config.h" + +#include "shortcuts/dzl-shortcut-label.h" +#include "shortcuts/dzl-shortcuts-shortcut.h" +#include "shortcuts/dzl-shortcuts-window-private.h" + +/** + * SECTION:dzl-shortcuts-shortcut + * @Title: DzlShortcutsShortcut + * @Short_description: Represents a keyboard shortcut in a DzlShortcutsWindow + * + * A DzlShortcutsShortcut represents a single keyboard shortcut or gesture + * with a short text. This widget is only meant to be used with #DzlShortcutsWindow. + */ + +struct _DzlShortcutsShortcut +{ + GtkBox parent_instance; + + GtkImage *image; + DzlShortcutLabel *accelerator; + GtkLabel *title; + GtkLabel *subtitle; + GtkLabel *title_box; + + GtkSizeGroup *accel_size_group; + GtkSizeGroup *title_size_group; + + gboolean subtitle_set; + gboolean icon_set; + GtkTextDirection direction; + gchar *action_name; + GtkShortcutType shortcut_type; +}; + +struct _DzlShortcutsShortcutClass +{ + GtkBoxClass parent_class; +}; + +G_DEFINE_TYPE (DzlShortcutsShortcut, dzl_shortcuts_shortcut, GTK_TYPE_BOX) + +enum { + PROP_0, + PROP_ACCELERATOR, + PROP_ICON, + PROP_ICON_SET, + PROP_TITLE, + PROP_SUBTITLE, + PROP_SUBTITLE_SET, + PROP_ACCEL_SIZE_GROUP, + PROP_TITLE_SIZE_GROUP, + PROP_DIRECTION, + PROP_SHORTCUT_TYPE, + PROP_ACTION_NAME, + LAST_PROP +}; + +static GParamSpec *properties[LAST_PROP]; + +static void +dzl_shortcuts_shortcut_set_accelerator (DzlShortcutsShortcut *self, + const gchar *accelerator) +{ + dzl_shortcut_label_set_accelerator (self->accelerator, accelerator); +} + +static void +dzl_shortcuts_shortcut_set_accel_size_group (DzlShortcutsShortcut *self, + GtkSizeGroup *group) +{ + if (self->accel_size_group) + { + gtk_size_group_remove_widget (self->accel_size_group, GTK_WIDGET (self->accelerator)); + gtk_size_group_remove_widget (self->accel_size_group, GTK_WIDGET (self->image)); + } + + if (group) + { + gtk_size_group_add_widget (group, GTK_WIDGET (self->accelerator)); + gtk_size_group_add_widget (group, GTK_WIDGET (self->image)); + } + + g_set_object (&self->accel_size_group, group); +} + +static void +dzl_shortcuts_shortcut_set_title_size_group (DzlShortcutsShortcut *self, + GtkSizeGroup *group) +{ + if (self->title_size_group) + gtk_size_group_remove_widget (self->title_size_group, GTK_WIDGET (self->title_box)); + if (group) + gtk_size_group_add_widget (group, GTK_WIDGET (self->title_box)); + + g_set_object (&self->title_size_group, group); +} + +static void +update_subtitle_from_type (DzlShortcutsShortcut *self) +{ + const gchar *subtitle; + + if (self->subtitle_set) + return; + + switch (self->shortcut_type) + { + case GTK_SHORTCUT_ACCELERATOR: + case GTK_SHORTCUT_GESTURE: + subtitle = NULL; + break; + + case GTK_SHORTCUT_GESTURE_PINCH: + subtitle = _("Two finger pinch"); + break; + + case GTK_SHORTCUT_GESTURE_STRETCH: + subtitle = _("Two finger stretch"); + break; + + case GTK_SHORTCUT_GESTURE_ROTATE_CLOCKWISE: + subtitle = _("Rotate clockwise"); + break; + + case GTK_SHORTCUT_GESTURE_ROTATE_COUNTERCLOCKWISE: + subtitle = _("Rotate counterclockwise"); + break; + + case GTK_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_LEFT: + subtitle = _("Two finger swipe left"); + break; + + case GTK_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_RIGHT: + subtitle = _("Two finger swipe right"); + break; + + default: + subtitle = NULL; + break; + } + + gtk_label_set_label (self->subtitle, subtitle); + gtk_widget_set_visible (GTK_WIDGET (self->subtitle), subtitle != NULL); + g_object_notify (G_OBJECT (self), "subtitle"); +} + +static void +dzl_shortcuts_shortcut_set_subtitle_set (DzlShortcutsShortcut *self, + gboolean subtitle_set) +{ + if (self->subtitle_set != subtitle_set) + { + self->subtitle_set = subtitle_set; + g_object_notify (G_OBJECT (self), "subtitle-set"); + } + update_subtitle_from_type (self); +} + +static void +dzl_shortcuts_shortcut_set_subtitle (DzlShortcutsShortcut *self, + const gchar *subtitle) +{ + gtk_label_set_label (self->subtitle, subtitle); + gtk_widget_set_visible (GTK_WIDGET (self->subtitle), subtitle && subtitle[0]); + dzl_shortcuts_shortcut_set_subtitle_set (self, subtitle && subtitle[0]); + + g_object_notify (G_OBJECT (self), "subtitle"); +} + +static void +update_icon_from_type (DzlShortcutsShortcut *self) +{ + GIcon *icon; + + if (self->icon_set) + return; + + switch (self->shortcut_type) + { + case GTK_SHORTCUT_GESTURE_PINCH: + icon = g_themed_icon_new ("gesture-pinch-symbolic"); + break; + + case GTK_SHORTCUT_GESTURE_STRETCH: + icon = g_themed_icon_new ("gesture-stretch-symbolic"); + break; + + case GTK_SHORTCUT_GESTURE_ROTATE_CLOCKWISE: + icon = g_themed_icon_new ("gesture-rotate-clockwise-symbolic"); + break; + + case GTK_SHORTCUT_GESTURE_ROTATE_COUNTERCLOCKWISE: + icon = g_themed_icon_new ("gesture-rotate-anticlockwise-symbolic"); + break; + + case GTK_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_LEFT: + icon = g_themed_icon_new ("gesture-two-finger-swipe-left-symbolic"); + break; + + case GTK_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_RIGHT: + icon = g_themed_icon_new ("gesture-two-finger-swipe-right-symbolic"); + break; + + case GTK_SHORTCUT_ACCELERATOR: + case GTK_SHORTCUT_GESTURE: + default: ; + icon = NULL; + break; + } + + if (icon) + { + gtk_image_set_from_gicon (self->image, icon, GTK_ICON_SIZE_DIALOG); + gtk_image_set_pixel_size (self->image, 64); + g_object_unref (icon); + } +} + +static void +dzl_shortcuts_shortcut_set_icon_set (DzlShortcutsShortcut *self, + gboolean icon_set) +{ + if (self->icon_set != icon_set) + { + self->icon_set = icon_set; + g_object_notify (G_OBJECT (self), "icon-set"); + } + update_icon_from_type (self); +} + +static void +dzl_shortcuts_shortcut_set_icon (DzlShortcutsShortcut *self, + GIcon *gicon) +{ + gtk_image_set_from_gicon (self->image, gicon, GTK_ICON_SIZE_DIALOG); + dzl_shortcuts_shortcut_set_icon_set (self, gicon != NULL); + g_object_notify (G_OBJECT (self), "icon"); +} + +static void +update_visible_from_direction (DzlShortcutsShortcut *self) +{ + if (self->direction == GTK_TEXT_DIR_NONE || + self->direction == gtk_widget_get_direction (GTK_WIDGET (self))) + { + gtk_widget_set_visible (GTK_WIDGET (self), TRUE); + gtk_widget_set_no_show_all (GTK_WIDGET (self), FALSE); + } + else + { + gtk_widget_set_visible (GTK_WIDGET (self), FALSE); + gtk_widget_set_no_show_all (GTK_WIDGET (self), TRUE); + } +} + +static void +dzl_shortcuts_shortcut_set_direction (DzlShortcutsShortcut *self, + GtkTextDirection direction) +{ + if (self->direction == direction) + return; + + self->direction = direction; + + update_visible_from_direction (self); + + g_object_notify (G_OBJECT (self), "direction"); +} + +static void +dzl_shortcuts_shortcut_direction_changed (GtkWidget *widget, + GtkTextDirection previous_dir) +{ + update_visible_from_direction (DZL_SHORTCUTS_SHORTCUT (widget)); + + GTK_WIDGET_CLASS (dzl_shortcuts_shortcut_parent_class)->direction_changed (widget, previous_dir); +} + +static void +dzl_shortcuts_shortcut_set_type (DzlShortcutsShortcut *self, + GtkShortcutType type) +{ + if (self->shortcut_type == type) + return; + + self->shortcut_type = type; + + update_subtitle_from_type (self); + update_icon_from_type (self); + + gtk_widget_set_visible (GTK_WIDGET (self->accelerator), type == GTK_SHORTCUT_ACCELERATOR); + gtk_widget_set_visible (GTK_WIDGET (self->image), type != GTK_SHORTCUT_ACCELERATOR); + + + g_object_notify (G_OBJECT (self), "shortcut-type"); +} + +static void +dzl_shortcuts_shortcut_set_action_name (DzlShortcutsShortcut *self, + const gchar *action_name) +{ + g_free (self->action_name); + self->action_name = g_strdup (action_name); + + g_object_notify (G_OBJECT (self), "action-name"); +} + +static void +dzl_shortcuts_shortcut_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutsShortcut *self = DZL_SHORTCUTS_SHORTCUT (object); + + switch (prop_id) + { + case PROP_TITLE: + g_value_set_string (value, gtk_label_get_label (self->title)); + break; + + case PROP_SUBTITLE: + g_value_set_string (value, gtk_label_get_label (self->subtitle)); + break; + + case PROP_SUBTITLE_SET: + g_value_set_boolean (value, self->subtitle_set); + break; + + case PROP_ACCELERATOR: + g_value_set_string (value, dzl_shortcut_label_get_accelerator (self->accelerator)); + break; + + case PROP_ICON: + { + GIcon *icon; + + gtk_image_get_gicon (self->image, &icon, NULL); + g_value_set_object (value, icon); + } + break; + + case PROP_ICON_SET: + g_value_set_boolean (value, self->icon_set); + break; + + case PROP_DIRECTION: + g_value_set_enum (value, self->direction); + break; + + case PROP_SHORTCUT_TYPE: + g_value_set_enum (value, self->shortcut_type); + break; + + case PROP_ACTION_NAME: + g_value_set_string (value, self->action_name); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcuts_shortcut_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutsShortcut *self = DZL_SHORTCUTS_SHORTCUT (object); + + switch (prop_id) + { + case PROP_ACCELERATOR: + dzl_shortcuts_shortcut_set_accelerator (self, g_value_get_string (value)); + break; + + case PROP_ICON: + dzl_shortcuts_shortcut_set_icon (self, g_value_get_object (value)); + break; + + case PROP_ICON_SET: + dzl_shortcuts_shortcut_set_icon_set (self, g_value_get_boolean (value)); + break; + + case PROP_ACCEL_SIZE_GROUP: + dzl_shortcuts_shortcut_set_accel_size_group (self, GTK_SIZE_GROUP (g_value_get_object (value))); + break; + + case PROP_TITLE: + gtk_label_set_label (self->title, g_value_get_string (value)); + break; + + case PROP_SUBTITLE: + dzl_shortcuts_shortcut_set_subtitle (self, g_value_get_string (value)); + break; + + case PROP_SUBTITLE_SET: + dzl_shortcuts_shortcut_set_subtitle_set (self, g_value_get_boolean (value)); + break; + + case PROP_TITLE_SIZE_GROUP: + dzl_shortcuts_shortcut_set_title_size_group (self, GTK_SIZE_GROUP (g_value_get_object (value))); + break; + + case PROP_DIRECTION: + dzl_shortcuts_shortcut_set_direction (self, g_value_get_enum (value)); + break; + + case PROP_SHORTCUT_TYPE: + dzl_shortcuts_shortcut_set_type (self, g_value_get_enum (value)); + break; + + case PROP_ACTION_NAME: + dzl_shortcuts_shortcut_set_action_name (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +dzl_shortcuts_shortcut_finalize (GObject *object) +{ + DzlShortcutsShortcut *self = DZL_SHORTCUTS_SHORTCUT (object); + + g_clear_object (&self->accel_size_group); + g_clear_object (&self->title_size_group); + g_free (self->action_name); + + G_OBJECT_CLASS (dzl_shortcuts_shortcut_parent_class)->finalize (object); +} + +static void +dzl_shortcuts_shortcut_add (GtkContainer *container, + GtkWidget *widget) +{ + g_warning ("Can't add children to %s", G_OBJECT_TYPE_NAME (container)); +} + +static GType +dzl_shortcuts_shortcut_child_type (GtkContainer *container) +{ + return G_TYPE_NONE; +} + +void +dzl_shortcuts_shortcut_update_accel (DzlShortcutsShortcut *self, + GtkWindow *window) +{ + GtkApplication *app; + gchar **accels; + gchar *str; + + if (self->action_name == NULL) + return; + + app = gtk_window_get_application (window); + if (app == NULL) + return; + + accels = gtk_application_get_accels_for_action (app, self->action_name); + str = g_strjoinv (" ", accels); + + dzl_shortcuts_shortcut_set_accelerator (self, str); + + g_free (str); + g_strfreev (accels); +} + +static void +dzl_shortcuts_shortcut_class_init (DzlShortcutsShortcutClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->finalize = dzl_shortcuts_shortcut_finalize; + object_class->get_property = dzl_shortcuts_shortcut_get_property; + object_class->set_property = dzl_shortcuts_shortcut_set_property; + + widget_class->direction_changed = dzl_shortcuts_shortcut_direction_changed; + + container_class->add = dzl_shortcuts_shortcut_add; + container_class->child_type = dzl_shortcuts_shortcut_child_type; + + /** + * DzlShortcutsShortcut:accelerator: + * + * The accelerator(s) represented by this object. This property is used + * if #DzlShortcutsShortcut:shortcut-type is set to #GTK_SHORTCUT_ACCELERATOR. + * + * The syntax of this property is (an extension of) the syntax understood by + * gtk_accelerator_parse(). Multiple accelerators can be specified by separating + * them with a space, but keep in mind that the available width is limited. + * It is also possible to specify ranges of shortcuts, using ... between the keys. + * Sequences of keys can be specified using a + or & between the keys. + * + * Examples: + * - A single shortcut: <ctl&rt;<alt>delete + * - Two alternative shortcuts: <shift>a Home + * - A range of shortcuts: <alt>1...<alt>9 + * - Several keys pressed together: Control_L&Control_R + * - A sequence of shortcuts or keys: <ctl>c+<ctl>x + * + * Use + instead of & when the keys may (or have to be) pressed sequentially (e.g + * use t+t for 'press the t key twice'). + * + * Note that <, > and & need to be escaped as <, > and & when used + * in .ui files. + */ + properties[PROP_ACCELERATOR] = + g_param_spec_string ("accelerator", + "Accelerator", + "The accelerator keys for shortcuts of type 'Accelerator'", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsShortcut:icon: + * + * An icon to represent the shortcut or gesture. This property is used if + * #DzlShortcutsShortcut:shortcut-type is set to #GTK_SHORTCUT_GESTURE. + * For the other predefined gesture types, GTK+ provides an icon on its own. + */ + properties[PROP_ICON] = + g_param_spec_object ("icon", + "Icon", + "The icon to show for shortcuts of type 'Other Gesture'", + G_TYPE_ICON, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsShortcut:icon-set: + * + * %TRUE if an icon has been set. + */ + properties[PROP_ICON_SET] = + g_param_spec_boolean ("icon-set", + "Icon Set", + "Whether an icon has been set", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsShortcut:title: + * + * The textual description for the shortcut or gesture represented by + * this object. This should be a short string that can fit in a single line. + */ + properties[PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "A short description for the shortcut", + "", + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsShortcut:subtitle: + * + * The subtitle for the shortcut or gesture. + * + * This is typically used for gestures and should be a short, one-line + * text that describes the gesture itself. For the predefined gesture + * types, GTK+ provides a subtitle on its own. + */ + properties[PROP_SUBTITLE] = + g_param_spec_string ("subtitle", + "Subtitle", + "A short description for the gesture", + "", + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsShortcut:subtitle-set: + * + * %TRUE if a subtitle has been set. + */ + properties[PROP_SUBTITLE_SET] = + g_param_spec_boolean ("subtitle-set", + "Subtitle Set", + "Whether a subtitle has been set", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsShortcut:accel-size-group: + * + * The size group for the accelerator portion of this shortcut. + * + * This is used internally by GTK+, and must not be modified by applications. + */ + properties[PROP_ACCEL_SIZE_GROUP] = + g_param_spec_object ("accel-size-group", + "Accelerator Size Group", + "Accelerator Size Group", + GTK_TYPE_SIZE_GROUP, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsShortcut:title-size-group: + * + * The size group for the textual portion of this shortcut. + * + * This is used internally by GTK+, and must not be modified by applications. + */ + properties[PROP_TITLE_SIZE_GROUP] = + g_param_spec_object ("title-size-group", + "Title Size Group", + "Title Size Group", + GTK_TYPE_SIZE_GROUP, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsShortcut:direction: + * + * The text direction for which this shortcut is active. If the shortcut + * is used regardless of the text direction, set this property to + * #GTK_TEXT_DIR_NONE. + */ + properties[PROP_DIRECTION] = + g_param_spec_enum ("direction", + "Direction", + "Text direction for which this shortcut is active", + GTK_TYPE_TEXT_DIRECTION, + GTK_TEXT_DIR_NONE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY)); + + /** + * DzlShortcutsShortcut:shortcut-type: + * + * The type of shortcut that is represented. + */ + properties[PROP_SHORTCUT_TYPE] = + g_param_spec_enum ("shortcut-type", + "Shortcut Type", + "The type of shortcut that is represented", + GTK_TYPE_SHORTCUT_TYPE, + GTK_SHORTCUT_ACCELERATOR, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY)); + + /** + * DzlShortcutsShortcut:action-name: + * + * A detailed action name. If this is set for a shortcut + * of type %GTK_SHORTCUT_ACCELERATOR, then GTK+ will use + * the accelerators that are associated with the action + * via gtk_application_set_accels_for_action(), and setting + * #DzlShortcutsShortcut::accelerator is not necessary. + * + * Since: 3.22 + */ + properties[PROP_ACTION_NAME] = + g_param_spec_string ("action-name", + "Action Name", + "The name of the action", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + gtk_widget_class_set_css_name (widget_class, "shortcut"); +} + +static void +dzl_shortcuts_shortcut_init (DzlShortcutsShortcut *self) +{ + gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_HORIZONTAL); + gtk_box_set_spacing (GTK_BOX (self), 12); + + self->direction = GTK_TEXT_DIR_NONE; + self->shortcut_type = GTK_SHORTCUT_ACCELERATOR; + + self->image = g_object_new (GTK_TYPE_IMAGE, + "visible", FALSE, + "valign", GTK_ALIGN_CENTER, + "no-show-all", TRUE, + NULL); + GTK_CONTAINER_CLASS (dzl_shortcuts_shortcut_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET (self->image)); + + self->accelerator = g_object_new (DZL_TYPE_SHORTCUT_LABEL, + "visible", TRUE, + "valign", GTK_ALIGN_CENTER, + "no-show-all", TRUE, + NULL); + GTK_CONTAINER_CLASS (dzl_shortcuts_shortcut_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET (self->accelerator)); + + self->title_box = g_object_new (GTK_TYPE_BOX, + "visible", TRUE, + "valign", GTK_ALIGN_CENTER, + "hexpand", TRUE, + "orientation", GTK_ORIENTATION_VERTICAL, + NULL); + GTK_CONTAINER_CLASS (dzl_shortcuts_shortcut_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET (self->title_box)); + + self->title = g_object_new (GTK_TYPE_LABEL, + "visible", TRUE, + "xalign", 0.0f, + NULL); + gtk_container_add (GTK_CONTAINER (self->title_box), GTK_WIDGET (self->title)); + + self->subtitle = g_object_new (GTK_TYPE_LABEL, + "visible", FALSE, + "no-show-all", TRUE, + "xalign", 0.0f, + NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self->subtitle)), + GTK_STYLE_CLASS_DIM_LABEL); + gtk_container_add (GTK_CONTAINER (self->title_box), GTK_WIDGET (self->subtitle)); +} diff --git a/src/shortcuts/dzl-shortcuts-shortcut.h b/src/shortcuts/dzl-shortcuts-shortcut.h new file mode 100644 index 0000000..9140250 --- /dev/null +++ b/src/shortcuts/dzl-shortcuts-shortcut.h @@ -0,0 +1,81 @@ +/* dzl-shortcuts-shortcutprivate.h + * + * Copyright (C) 2015 Christian Hergert + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#ifndef DZL_SHORTCUTS_SHORTCUT_H +#define DZL_SHORTCUTS_SHORTCUT_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUTS_SHORTCUT (dzl_shortcuts_shortcut_get_type()) +#define DZL_SHORTCUTS_SHORTCUT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), DZL_TYPE_SHORTCUTS_SHORTCUT, DzlShortcutsShortcut)) +#define DZL_SHORTCUTS_SHORTCUT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), DZL_TYPE_SHORTCUTS_SHORTCUT, DzlShortcutsShortcutClass)) +#define DZL_IS_SHORTCUTS_SHORTCUT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), DZL_TYPE_SHORTCUTS_SHORTCUT)) +#define DZL_IS_SHORTCUTS_SHORTCUT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), DZL_TYPE_SHORTCUTS_SHORTCUT)) +#define DZL_SHORTCUTS_SHORTCUT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), DZL_TYPE_SHORTCUTS_SHORTCUT, DzlShortcutsShortcutClass)) + + +typedef struct _DzlShortcutsShortcut DzlShortcutsShortcut; +typedef struct _DzlShortcutsShortcutClass DzlShortcutsShortcutClass; + +/** + * DzlShortcutType: + * @DZL_SHORTCUT_ACCELERATOR: + * The shortcut is a keyboard accelerator. The #DzlShortcutsShortcut:accelerator + * property will be used. + * @DZL_SHORTCUT_GESTURE_PINCH: + * The shortcut is a pinch gesture. GTK+ provides an icon and subtitle. + * @DZL_SHORTCUT_GESTURE_STRETCH: + * The shortcut is a stretch gesture. GTK+ provides an icon and subtitle. + * @DZL_SHORTCUT_GESTURE_ROTATE_CLOCKWISE: + * The shortcut is a clockwise rotation gesture. GTK+ provides an icon and subtitle. + * @DZL_SHORTCUT_GESTURE_ROTATE_COUNTERCLOCKWISE: + * The shortcut is a counterclockwise rotation gesture. GTK+ provides an icon and subtitle. + * @DZL_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_LEFT: + * The shortcut is a two-finger swipe gesture. GTK+ provides an icon and subtitle. + * @DZL_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_RIGHT: + * The shortcut is a two-finger swipe gesture. GTK+ provides an icon and subtitle. + * @DZL_SHORTCUT_GESTURE: + * The shortcut is a gesture. The #DzlShortcutsShortcut:icon property will be + * used. + * + * DzlShortcutType specifies the kind of shortcut that is being described. + * More values may be added to this enumeration over time. + * + * Since: 3.20 + */ +typedef enum { + DZL_SHORTCUT_ACCELERATOR, + DZL_SHORTCUT_GESTURE_PINCH, + DZL_SHORTCUT_GESTURE_STRETCH, + DZL_SHORTCUT_GESTURE_ROTATE_CLOCKWISE, + DZL_SHORTCUT_GESTURE_ROTATE_COUNTERCLOCKWISE, + DZL_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_LEFT, + DZL_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_RIGHT, + DZL_SHORTCUT_GESTURE +} DzlShortcutType; + +DZL_AVAILABLE_IN_ALL +GType dzl_shortcuts_shortcut_get_type (void) G_GNUC_CONST; + +G_END_DECLS + +#endif /* DZL_SHORTCUTS_SHORTCUT_H */ diff --git a/src/shortcuts/dzl-shortcuts-window-private.h b/src/shortcuts/dzl-shortcuts-window-private.h new file mode 100644 index 0000000..a2f9980 --- /dev/null +++ b/src/shortcuts/dzl-shortcuts-window-private.h @@ -0,0 +1,38 @@ + +/* GTK - The GIMP Toolkit + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +/* + * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS + * file for a list of people on the GTK+ Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GTK+ at ftp://ftp.gtk.org/pub/gtk/. + */ + +#ifndef __DZL_SHORTCUTS_WINDOW_PRIVATE_H__ +#define __DZL_SHORTCUTS_WINDOW_PRIVATE_H__ + +#include "dzl-shortcuts-window.h" + +G_BEGIN_DECLS + +void dzl_shortcuts_window_set_window (DzlShortcutsWindow *self, + GtkWindow *window); + +G_END_DECLS + +#endif /* __GTK_sHORTCUTS_WINDOW_PRIVATE_H__ */ diff --git a/src/shortcuts/dzl-shortcuts-window.c b/src/shortcuts/dzl-shortcuts-window.c new file mode 100644 index 0000000..71e0b2b --- /dev/null +++ b/src/shortcuts/dzl-shortcuts-window.c @@ -0,0 +1,1065 @@ +/* dzl-shortcuts-window.c + * + * Copyright (C) 2015 Christian Hergert + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#include + +#include "config.h" + +#include "shortcuts/dzl-shortcuts-window.h" +#include "shortcuts/dzl-shortcuts-section.h" +#include "shortcuts/dzl-shortcuts-group.h" +#include "shortcuts/dzl-shortcuts-shortcut-private.h" + +/** + * SECTION:dzl-shortcuts-window + * @Title: DzlShortcutsWindow + * @Short_description: Toplevel which shows help for shortcuts + * + * A DzlShortcutsWindow shows brief information about the keyboard shortcuts + * and gestures of an application. The shortcuts can be grouped, and you can + * have multiple sections in this window, corresponding to the major modes of + * your application. + * + * Additionally, the shortcuts can be filtered by the current view, to avoid + * showing information that is not relevant in the current application context. + * + * The recommended way to construct a DzlShortcutsWindow is with GtkBuilder, + * by populating a #DzlShortcutsWindow with one or more #DzlShortcutsSection + * objects, which contain #DzlShortcutsGroups that in turn contain objects of + * class #DzlShortcutsShortcut. + * + * # A simple example: + * + * ![](gedit-shortcuts.png) + * + * This example has as single section. As you can see, the shortcut groups + * are arranged in columns, and spread across several pages if there are too + * many to find on a single page. + * + * The .ui file for this example can be found [here](https://git.gnome.org/browse/gtk+/tree/demos/gtk-demo/shortcuts-gedit.ui). + * + * # An example with multiple views: + * + * ![](clocks-shortcuts.png) + * + * This example shows a #DzlShortcutsWindow that has been configured to show only + * the shortcuts relevant to the "stopwatch" view. + * + * The .ui file for this example can be found [here](https://git.gnome.org/browse/gtk+/tree/demos/gtk-demo/shortcuts-clocks.ui). + * + * # An example with multiple sections: + * + * ![](builder-shortcuts.png) + * + * This example shows a #DzlShortcutsWindow with two sections, "Editor Shortcuts" + * and "Terminal Shortcuts". + * + * The .ui file for this example can be found [here](https://git.gnome.org/browse/gtk+/tree/demos/gtk-demo/shortcuts-builder.ui). + */ + +typedef struct +{ + GHashTable *keywords; + gchar *initial_section; + gchar *last_section_name; + gchar *view_name; + GtkSizeGroup *search_text_group; + GtkSizeGroup *search_image_group; + GHashTable *search_items_hash; + + GtkStack *stack; + GtkStack *title_stack; + GtkMenuButton *menu_button; + GtkLabel *menu_label; + GtkSearchBar *search_bar; + GtkSearchEntry *search_entry; + GtkHeaderBar *header_bar; + GtkWidget *main_box; + GtkPopover *popover; + GtkListBox *list_box; + GtkBox *search_gestures; + GtkBox *search_shortcuts; + + GtkWindow *window; + gulong keys_changed_id; +} DzlShortcutsWindowPrivate; + +typedef struct +{ + DzlShortcutsWindow *self; + GtkBuilder *builder; + GQueue *stack; + gchar *property_name; + guint translatable : 1; +} ViewsParserData; + + +G_DEFINE_TYPE_WITH_PRIVATE (DzlShortcutsWindow, dzl_shortcuts_window, GTK_TYPE_WINDOW) + + +enum { + CLOSE, + SEARCH, + LAST_SIGNAL +}; + +enum { + PROP_0, + PROP_SECTION_NAME, + PROP_VIEW_NAME, + LAST_PROP +}; + +static GParamSpec *properties[LAST_PROP]; +static guint signals[LAST_SIGNAL]; + + +static gint +number_of_children (GtkContainer *container) +{ + GList *children; + gint n; + + children = gtk_container_get_children (container); + n = g_list_length (children); + g_list_free (children); + + return n; +} + +static void +update_title_stack (DzlShortcutsWindow *self) +{ + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + GtkWidget *visible_child; + + visible_child = gtk_stack_get_visible_child (priv->stack); + + if (DZL_IS_SHORTCUTS_SECTION (visible_child)) + { + if (number_of_children (GTK_CONTAINER (priv->stack)) > 3) + { + gchar *title; + + gtk_stack_set_visible_child_name (priv->title_stack, "sections"); + g_object_get (visible_child, "title", &title, NULL); + gtk_label_set_label (priv->menu_label, title); + g_free (title); + } + else + { + gtk_stack_set_visible_child_name (priv->title_stack, "title"); + } + } + else if (visible_child != NULL) + { + gtk_stack_set_visible_child_name (priv->title_stack, "search"); + } +} + +static void +dzl_shortcuts_window_add_search_item (GtkWidget *child, gpointer data) +{ + DzlShortcutsWindow *self = data; + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + GtkWidget *item; + gchar *accelerator = NULL; + gchar *title = NULL; + gchar *hash_key = NULL; + GIcon *icon = NULL; + gboolean icon_set = FALSE; + gboolean subtitle_set = FALSE; + GtkTextDirection direction; + GtkShortcutType shortcut_type; + gchar *action_name = NULL; + gchar *subtitle; + gchar *str; + gchar *keywords; + + if (DZL_IS_SHORTCUTS_SHORTCUT (child)) + { + GEnumClass *class; + GEnumValue *value; + + g_object_get (child, + "accelerator", &accelerator, + "title", &title, + "direction", &direction, + "icon-set", &icon_set, + "subtitle-set", &subtitle_set, + "shortcut-type", &shortcut_type, + "action-name", &action_name, + NULL); + + class = G_ENUM_CLASS (g_type_class_ref (GTK_TYPE_SHORTCUT_TYPE)); + value = g_enum_get_value (class, shortcut_type); + + hash_key = g_strdup_printf ("%s-%s-%s", title, value->value_nick, accelerator); + + g_type_class_unref (class); + + if (g_hash_table_contains (priv->search_items_hash, hash_key)) + { + g_free (hash_key); + g_free (title); + g_free (accelerator); + return; + } + + g_hash_table_insert (priv->search_items_hash, hash_key, GINT_TO_POINTER (1)); + + item = g_object_new (DZL_TYPE_SHORTCUTS_SHORTCUT, + "accelerator", accelerator, + "title", title, + "direction", direction, + "shortcut-type", shortcut_type, + "accel-size-group", priv->search_image_group, + "title-size-group", priv->search_text_group, + "action-name", action_name, + NULL); + if (icon_set) + { + g_object_get (child, "icon", &icon, NULL); + g_object_set (item, "icon", icon, NULL); + g_clear_object (&icon); + } + if (subtitle_set) + { + g_object_get (child, "subtitle", &subtitle, NULL); + g_object_set (item, "subtitle", subtitle, NULL); + g_free (subtitle); + } + str = g_strdup_printf ("%s %s", accelerator, title); + keywords = g_utf8_strdown (str, -1); + + g_hash_table_insert (priv->keywords, item, keywords); + if (shortcut_type == GTK_SHORTCUT_ACCELERATOR) + gtk_container_add (GTK_CONTAINER (priv->search_shortcuts), item); + else + gtk_container_add (GTK_CONTAINER (priv->search_gestures), item); + + g_free (title); + g_free (accelerator); + g_free (str); + g_free (action_name); + } + else if (GTK_IS_CONTAINER (child)) + { + gtk_container_foreach (GTK_CONTAINER (child), dzl_shortcuts_window_add_search_item, self); + } +} + +static void +section_notify_cb (GObject *section, + GParamSpec *pspec, + gpointer data) +{ + DzlShortcutsWindow *self = data; + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + if (strcmp (pspec->name, "section-name") == 0) + { + gchar *name; + + g_object_get (section, "section-name", &name, NULL); + gtk_container_child_set (GTK_CONTAINER (priv->stack), GTK_WIDGET (section), "name", name, NULL); + g_free (name); + } + else if (strcmp (pspec->name, "title") == 0) + { + gchar *title; + GtkWidget *label; + + label = g_object_get_data (section, "gtk-shortcuts-title"); + g_object_get (section, "title", &title, NULL); + gtk_label_set_label (GTK_LABEL (label), title); + g_free (title); + } +} + +static void +dzl_shortcuts_window_add_section (DzlShortcutsWindow *self, + DzlShortcutsSection *section) +{ + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + GtkListBoxRow *row; + gchar *title; + gchar *name; + const gchar *visible_section; + GtkWidget *label; + + gtk_container_foreach (GTK_CONTAINER (section), dzl_shortcuts_window_add_search_item, self); + + g_object_get (section, + "section-name", &name, + "title", &title, + NULL); + + g_signal_connect (section, "notify", G_CALLBACK (section_notify_cb), self); + + if (name == NULL) + name = g_strdup ("shortcuts"); + + gtk_stack_add_titled (priv->stack, GTK_WIDGET (section), name, title); + + visible_section = gtk_stack_get_visible_child_name (priv->stack); + if (strcmp (visible_section, "internal-search") == 0 || + (priv->initial_section && strcmp (priv->initial_section, visible_section) == 0)) + gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (section)); + + row = g_object_new (GTK_TYPE_LIST_BOX_ROW, + "visible", TRUE, + NULL); + g_object_set_data (G_OBJECT (row), "gtk-shortcuts-section", section); + label = g_object_new (GTK_TYPE_LABEL, + "margin", 6, + "label", title, + "xalign", 0.5f, + "visible", TRUE, + NULL); + g_object_set_data (G_OBJECT (section), "gtk-shortcuts-title", label); + gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (label)); + gtk_container_add (GTK_CONTAINER (priv->list_box), GTK_WIDGET (row)); + + update_title_stack (self); + + g_free (name); + g_free (title); +} + +static void +dzl_shortcuts_window_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlShortcutsWindow *self = (DzlShortcutsWindow *)container; + + if (DZL_IS_SHORTCUTS_SECTION (widget)) + dzl_shortcuts_window_add_section (self, DZL_SHORTCUTS_SECTION (widget)); + else + g_warning ("Can't add children of type %s to %s", + G_OBJECT_TYPE_NAME (widget), + G_OBJECT_TYPE_NAME (container)); +} + +static void +dzl_shortcuts_window_remove (GtkContainer *container, + GtkWidget *widget) +{ + DzlShortcutsWindow *self = (DzlShortcutsWindow *)container; + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + g_signal_handlers_disconnect_by_func (widget, section_notify_cb, self); + + if (widget == (GtkWidget *)priv->header_bar || + widget == (GtkWidget *)priv->main_box) + GTK_CONTAINER_CLASS (dzl_shortcuts_window_parent_class)->remove (container, widget); + else + gtk_container_remove (GTK_CONTAINER (priv->stack), widget); +} + +static void +dzl_shortcuts_window_forall (GtkContainer *container, + gboolean include_internal, + GtkCallback callback, + gpointer callback_data) +{ + DzlShortcutsWindow *self = (DzlShortcutsWindow *)container; + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + if (include_internal) + { + GTK_CONTAINER_CLASS (dzl_shortcuts_window_parent_class)->forall (container, include_internal, callback, callback_data); + } + else + { + if (priv->stack) + { + GList *children, *l; + GtkWidget *search; + GtkWidget *empty; + + search = gtk_stack_get_child_by_name (GTK_STACK (priv->stack), "internal-search"); + empty = gtk_stack_get_child_by_name (GTK_STACK (priv->stack), "no-search-results"); + children = gtk_container_get_children (GTK_CONTAINER (priv->stack)); + for (l = children; l; l = l->next) + { + GtkWidget *child = l->data; + + if (include_internal || + (child != search && child != empty)) + callback (child, callback_data); + } + g_list_free (children); + } + } +} + +static void +dzl_shortcuts_window_set_view_name (DzlShortcutsWindow *self, + const gchar *view_name) +{ + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + GList *sections, *l; + + g_free (priv->view_name); + priv->view_name = g_strdup (view_name); + + sections = gtk_container_get_children (GTK_CONTAINER (priv->stack)); + for (l = sections; l; l = l->next) + { + DzlShortcutsSection *section = l->data; + + if (DZL_IS_SHORTCUTS_SECTION (section)) + g_object_set (section, "view-name", priv->view_name, NULL); + } + g_list_free (sections); +} + +static void +dzl_shortcuts_window_set_section_name (DzlShortcutsWindow *self, + const gchar *section_name) +{ + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + GtkWidget *section = NULL; + + g_free (priv->initial_section); + priv->initial_section = g_strdup (section_name); + + if (section_name) + section = gtk_stack_get_child_by_name (priv->stack, section_name); + if (section) + gtk_stack_set_visible_child (priv->stack, section); +} + +static void +update_accels_cb (GtkWidget *widget, + gpointer data) +{ + DzlShortcutsWindow *self = data; + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + if (DZL_IS_SHORTCUTS_SHORTCUT (widget)) + dzl_shortcuts_shortcut_update_accel (DZL_SHORTCUTS_SHORTCUT (widget), priv->window); + else if (GTK_IS_CONTAINER (widget)) + gtk_container_foreach (GTK_CONTAINER (widget), update_accels_cb, self); +} + +static void +update_accels_for_actions (DzlShortcutsWindow *self) +{ + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + if (priv->window) + gtk_container_forall (GTK_CONTAINER (self), update_accels_cb, self); +} + +static void +keys_changed_handler (GtkWindow *window, + DzlShortcutsWindow *self) +{ + update_accels_for_actions (self); +} + +void +dzl_shortcuts_window_set_window (DzlShortcutsWindow *self, + GtkWindow *window) +{ + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + if (priv->keys_changed_id) + { + g_signal_handler_disconnect (priv->window, priv->keys_changed_id); + priv->keys_changed_id = 0; + } + + priv->window = window; + + if (priv->window) + priv->keys_changed_id = g_signal_connect (window, "keys-changed", + G_CALLBACK (keys_changed_handler), + self); + + update_accels_for_actions (self); +} + +static void +dzl_shortcuts_window__list_box__row_activated (DzlShortcutsWindow *self, + GtkListBoxRow *row, + GtkListBox *list_box) +{ + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + GtkWidget *section; + + section = g_object_get_data (G_OBJECT (row), "gtk-shortcuts-section"); + gtk_stack_set_visible_child (priv->stack, section); + gtk_popover_popdown (priv->popover); +} + +static gboolean +hidden_by_direction (GtkWidget *widget) +{ + if (DZL_IS_SHORTCUTS_SHORTCUT (widget)) + { + GtkTextDirection dir; + + g_object_get (widget, "direction", &dir, NULL); + if (dir != GTK_TEXT_DIR_NONE && + dir != gtk_widget_get_direction (widget)) + return TRUE; + } + + return FALSE; +} + +static void +dzl_shortcuts_window__entry__changed (DzlShortcutsWindow *self, + GtkSearchEntry *search_entry) +{ + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + gchar *downcase = NULL; + GHashTableIter iter; + const gchar *text; + const gchar *last_section_name; + gpointer key; + gpointer value; + gboolean has_result; + + text = gtk_entry_get_text (GTK_ENTRY (search_entry)); + + if (!text || !*text) + { + if (priv->last_section_name != NULL) + { + gtk_stack_set_visible_child_name (priv->stack, priv->last_section_name); + return; + + } + } + + last_section_name = gtk_stack_get_visible_child_name (priv->stack); + + if (g_strcmp0 (last_section_name, "internal-search") != 0 && + g_strcmp0 (last_section_name, "no-search-results") != 0) + { + g_free (priv->last_section_name); + priv->last_section_name = g_strdup (last_section_name); + } + + downcase = g_utf8_strdown (text, -1); + + g_hash_table_iter_init (&iter, priv->keywords); + + has_result = FALSE; + while (g_hash_table_iter_next (&iter, &key, &value)) + { + GtkWidget *widget = key; + const gchar *keywords = value; + gboolean match; + + if (hidden_by_direction (widget)) + match = FALSE; + else + match = strstr (keywords, downcase) != NULL; + + gtk_widget_set_visible (widget, match); + has_result |= match; + } + + g_free (downcase); + + if (has_result) + gtk_stack_set_visible_child_name (priv->stack, "internal-search"); + else + gtk_stack_set_visible_child_name (priv->stack, "no-search-results"); +} + +static void +dzl_shortcuts_window__search_mode__changed (DzlShortcutsWindow *self) +{ + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + if (!gtk_search_bar_get_search_mode (priv->search_bar)) + { + if (priv->last_section_name != NULL) + gtk_stack_set_visible_child_name (priv->stack, priv->last_section_name); + } +} + +static void +dzl_shortcuts_window_close (DzlShortcutsWindow *self) +{ + gtk_window_close (GTK_WINDOW (self)); +} + +static void +dzl_shortcuts_window_search (DzlShortcutsWindow *self) +{ + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + gtk_search_bar_set_search_mode (priv->search_bar, TRUE); +} + +static void +dzl_shortcuts_window_constructed (GObject *object) +{ + DzlShortcutsWindow *self = (DzlShortcutsWindow *)object; + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + G_OBJECT_CLASS (dzl_shortcuts_window_parent_class)->constructed (object); + + if (priv->initial_section != NULL) + gtk_stack_set_visible_child_name (priv->stack, priv->initial_section); +} + +static void +dzl_shortcuts_window_finalize (GObject *object) +{ + DzlShortcutsWindow *self = (DzlShortcutsWindow *)object; + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + g_clear_pointer (&priv->keywords, g_hash_table_unref); + g_clear_pointer (&priv->initial_section, g_free); + g_clear_pointer (&priv->view_name, g_free); + g_clear_pointer (&priv->last_section_name, g_free); + g_clear_pointer (&priv->search_items_hash, g_hash_table_unref); + + g_clear_object (&priv->search_image_group); + g_clear_object (&priv->search_text_group); + + G_OBJECT_CLASS (dzl_shortcuts_window_parent_class)->finalize (object); +} + +static void +dzl_shortcuts_window_dispose (GObject *object) +{ + DzlShortcutsWindow *self = (DzlShortcutsWindow *)object; + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + g_signal_handlers_disconnect_by_func (priv->stack, G_CALLBACK (update_title_stack), self); + + dzl_shortcuts_window_set_window (self, NULL); + + if (priv->header_bar) + { + gtk_widget_destroy (GTK_WIDGET (priv->header_bar)); + priv->header_bar = NULL; + priv->popover = NULL; + } + + G_OBJECT_CLASS (dzl_shortcuts_window_parent_class)->dispose (object); + +#if 0 + if (priv->main_box) + { + gtk_widget_destroy (GTK_WIDGET (priv->main_box)); + priv->main_box = NULL; + } +#endif +} + +static void +dzl_shortcuts_window_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlShortcutsWindow *self = (DzlShortcutsWindow *)object; + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + switch (prop_id) + { + case PROP_SECTION_NAME: + { + GtkWidget *child = gtk_stack_get_visible_child (priv->stack); + + if (child != NULL) + { + gchar *name = NULL; + + gtk_container_child_get (GTK_CONTAINER (priv->stack), child, + "name", &name, + NULL); + g_value_take_string (value, name); + } + } + break; + + case PROP_VIEW_NAME: + g_value_set_string (value, priv->view_name); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcuts_window_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlShortcutsWindow *self = (DzlShortcutsWindow *)object; + + switch (prop_id) + { + case PROP_SECTION_NAME: + dzl_shortcuts_window_set_section_name (self, g_value_get_string (value)); + break; + + case PROP_VIEW_NAME: + dzl_shortcuts_window_set_view_name (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_shortcuts_window_unmap (GtkWidget *widget) +{ + DzlShortcutsWindow *self = (DzlShortcutsWindow *)widget; + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + gtk_search_bar_set_search_mode (priv->search_bar, FALSE); + + GTK_WIDGET_CLASS (dzl_shortcuts_window_parent_class)->unmap (widget); +} + +static GType +dzl_shortcuts_window_child_type (GtkContainer *container) +{ + return GTK_TYPE_SHORTCUTS_SECTION; +} + +static void +dzl_shortcuts_window_class_init (DzlShortcutsWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + GtkBindingSet *binding_set = gtk_binding_set_by_class (klass); + + object_class->constructed = dzl_shortcuts_window_constructed; + object_class->finalize = dzl_shortcuts_window_finalize; + object_class->get_property = dzl_shortcuts_window_get_property; + object_class->set_property = dzl_shortcuts_window_set_property; + object_class->dispose = dzl_shortcuts_window_dispose; + + widget_class->unmap = dzl_shortcuts_window_unmap; + container_class->add = dzl_shortcuts_window_add; + container_class->remove = dzl_shortcuts_window_remove; + container_class->child_type = dzl_shortcuts_window_child_type; + container_class->forall = dzl_shortcuts_window_forall; + + klass->close = dzl_shortcuts_window_close; + klass->search = dzl_shortcuts_window_search; + + /** + * DzlShortcutsWindow:section-name: + * + * The name of the section to show. + * + * This should be the section-name of one of the #DzlShortcutsSection + * objects that are in this shortcuts window. + */ + properties[PROP_SECTION_NAME] = + g_param_spec_string ("section-name", _("Section Name"), _("Section Name"), + "internal-search", + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * DzlShortcutsWindow:view-name: + * + * The view name by which to filter the contents. + * + * This should correspond to the #DzlShortcutsGroup:view property of some of + * the #DzlShortcutsGroup objects that are inside this shortcuts window. + * + * Set this to %NULL to show all groups. + */ + properties[PROP_VIEW_NAME] = + g_param_spec_string ("view-name", _("View Name"), _("View Name"), + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + /** + * DzlShortcutsWindow::close: + * + * The ::close signal is a + * [keybinding signal][GtkBindingSignal] + * which gets emitted when the user uses a keybinding to close + * the window. + * + * The default binding for this signal is the Escape key. + */ + signals[CLOSE] = g_signal_new (_("close"), + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (DzlShortcutsWindowClass, close), + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + /** + * DzlShortcutsWindow::search: + * + * The ::search signal is a + * [keybinding signal][GtkBindingSignal] + * which gets emitted when the user uses a keybinding to start a search. + * + * The default binding for this signal is Control-F. + */ + signals[SEARCH] = g_signal_new (_("search"), + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (DzlShortcutsWindowClass, search), + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "close", 0); + gtk_binding_entry_add_signal (binding_set, GDK_KEY_f, GDK_CONTROL_MASK, "search", 0); + + g_type_ensure (GTK_TYPE_SHORTCUTS_GROUP); + g_type_ensure (GTK_TYPE_SHORTCUTS_SHORTCUT); +} + +static gboolean +window_key_press_event_cb (GtkWidget *window, + GdkEvent *event, + gpointer data) +{ + DzlShortcutsWindow *self = DZL_SHORTCUTS_WINDOW (window); + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + + return gtk_search_bar_handle_event (priv->search_bar, event); +} + +static void +dzl_shortcuts_window_init (DzlShortcutsWindow *self) +{ + DzlShortcutsWindowPrivate *priv = dzl_shortcuts_window_get_instance_private (self); + GtkToggleButton *search_button; + GtkBox *menu_box; + GtkBox *box; + GtkArrow *arrow; + GtkWidget *scroller; + GtkWidget *label; + GtkWidget *empty; + PangoAttrList *attributes; + + gtk_window_set_resizable (GTK_WINDOW (self), FALSE); + gtk_window_set_type_hint (GTK_WINDOW (self), GDK_WINDOW_TYPE_HINT_DIALOG); + + g_signal_connect (self, "key-press-event", + G_CALLBACK (window_key_press_event_cb), NULL); + + priv->keywords = g_hash_table_new_full (NULL, NULL, NULL, g_free); + priv->search_items_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + priv->search_text_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + priv->search_image_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + priv->header_bar = g_object_new (GTK_TYPE_HEADER_BAR, + "show-close-button", TRUE, + "visible", TRUE, + NULL); + gtk_window_set_titlebar (GTK_WINDOW (self), GTK_WIDGET (priv->header_bar)); + + search_button = g_object_new (GTK_TYPE_TOGGLE_BUTTON, + "child", g_object_new (GTK_TYPE_IMAGE, + "visible", TRUE, + "icon-name", "edit-find-symbolic", + NULL), + "visible", TRUE, + NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (search_button)), "image-button"); + gtk_container_add (GTK_CONTAINER (priv->header_bar), GTK_WIDGET (search_button)); + + priv->main_box = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_VERTICAL, + "visible", TRUE, + NULL); + GTK_CONTAINER_CLASS (dzl_shortcuts_window_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET (priv->main_box)); + + priv->search_bar = g_object_new (GTK_TYPE_SEARCH_BAR, + "visible", TRUE, + NULL); + g_object_bind_property (priv->search_bar, "search-mode-enabled", + search_button, "active", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + gtk_container_add (GTK_CONTAINER (priv->main_box), GTK_WIDGET (priv->search_bar)); + + priv->stack = g_object_new (GTK_TYPE_STACK, + "expand", TRUE, + "homogeneous", TRUE, + "transition-type", GTK_STACK_TRANSITION_TYPE_CROSSFADE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (priv->main_box), GTK_WIDGET (priv->stack)); + + priv->title_stack = g_object_new (GTK_TYPE_STACK, + "visible", TRUE, + NULL); + gtk_header_bar_set_custom_title (priv->header_bar, GTK_WIDGET (priv->title_stack)); + + label = gtk_label_new (_("Shortcuts")); + gtk_widget_show (label); + gtk_style_context_add_class (gtk_widget_get_style_context (label), GTK_STYLE_CLASS_TITLE); + gtk_stack_add_named (priv->title_stack, label, "title"); + + label = gtk_label_new (_("Search Results")); + gtk_widget_show (label); + gtk_style_context_add_class (gtk_widget_get_style_context (label), GTK_STYLE_CLASS_TITLE); + gtk_stack_add_named (priv->title_stack, label, "search"); + + priv->menu_button = g_object_new (GTK_TYPE_MENU_BUTTON, + "focus-on-click", FALSE, + "visible", TRUE, + "relief", GTK_RELIEF_NONE, + NULL); + gtk_stack_add_named (priv->title_stack, GTK_WIDGET (priv->menu_button), "sections"); + + menu_box = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "spacing", 6, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (priv->menu_button), GTK_WIDGET (menu_box)); + + priv->menu_label = g_object_new (GTK_TYPE_LABEL, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (menu_box), GTK_WIDGET (priv->menu_label)); + + G_GNUC_BEGIN_IGNORE_DEPRECATIONS; + arrow = g_object_new (GTK_TYPE_ARROW, + "arrow-type", GTK_ARROW_DOWN, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (menu_box), GTK_WIDGET (arrow)); + G_GNUC_END_IGNORE_DEPRECATIONS; + + priv->popover = g_object_new (GTK_TYPE_POPOVER, + "border-width", 6, + "relative-to", priv->menu_button, + "position", GTK_POS_BOTTOM, + NULL); + gtk_menu_button_set_popover (priv->menu_button, GTK_WIDGET (priv->popover)); + + priv->list_box = g_object_new (GTK_TYPE_LIST_BOX, + "selection-mode", GTK_SELECTION_NONE, + "visible", TRUE, + NULL); + g_signal_connect_object (priv->list_box, + "row-activated", + G_CALLBACK (dzl_shortcuts_window__list_box__row_activated), + self, + G_CONNECT_SWAPPED); + gtk_container_add (GTK_CONTAINER (priv->popover), GTK_WIDGET (priv->list_box)); + + priv->search_entry = GTK_SEARCH_ENTRY (gtk_search_entry_new ()); + gtk_widget_show (GTK_WIDGET (priv->search_entry)); + gtk_container_add (GTK_CONTAINER (priv->search_bar), GTK_WIDGET (priv->search_entry)); + g_object_set (priv->search_entry, + "placeholder-text", _("Search Shortcuts"), + "width-chars", 40, + NULL); + g_signal_connect_object (priv->search_entry, + "search-changed", + G_CALLBACK (dzl_shortcuts_window__entry__changed), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (priv->search_bar, + "notify::search-mode-enabled", + G_CALLBACK (dzl_shortcuts_window__search_mode__changed), + self, + G_CONNECT_SWAPPED); + + scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW, + "visible", TRUE, + NULL); + box = g_object_new (GTK_TYPE_BOX, + "border-width", 24, + "halign", GTK_ALIGN_CENTER, + "spacing", 24, + "orientation", GTK_ORIENTATION_VERTICAL, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (box)); + gtk_stack_add_named (priv->stack, scroller, "internal-search"); + + priv->search_shortcuts = g_object_new (GTK_TYPE_BOX, + "halign", GTK_ALIGN_CENTER, + "spacing", 6, + "orientation", GTK_ORIENTATION_VERTICAL, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->search_shortcuts)); + + priv->search_gestures = g_object_new (GTK_TYPE_BOX, + "halign", GTK_ALIGN_CENTER, + "spacing", 6, + "orientation", GTK_ORIENTATION_VERTICAL, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->search_gestures)); + + empty = g_object_new (GTK_TYPE_GRID, + "visible", TRUE, + "row-spacing", 12, + "margin", 12, + "hexpand", TRUE, + "vexpand", TRUE, + "halign", GTK_ALIGN_CENTER, + "valign", GTK_ALIGN_CENTER, + NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (empty), GTK_STYLE_CLASS_DIM_LABEL); + gtk_grid_attach (GTK_GRID (empty), + g_object_new (GTK_TYPE_IMAGE, + "visible", TRUE, + "icon-name", "edit-find-symbolic", + "pixel-size", 72, + NULL), + 0, 0, 1, 1); + attributes = pango_attr_list_new (); + pango_attr_list_insert (attributes, pango_attr_weight_new (PANGO_WEIGHT_BOLD)); + pango_attr_list_insert (attributes, pango_attr_scale_new (1.44)); + label = g_object_new (GTK_TYPE_LABEL, + "visible", TRUE, + "label", _("No Results Found"), + "attributes", attributes, + NULL); + pango_attr_list_unref (attributes); + gtk_grid_attach (GTK_GRID (empty), label, 0, 1, 1, 1); + label = g_object_new (GTK_TYPE_LABEL, + "visible", TRUE, + "label", _("Try a different search"), + NULL); + gtk_grid_attach (GTK_GRID (empty), label, 0, 2, 1, 1); + + gtk_stack_add_named (priv->stack, empty, "no-search-results"); + + g_signal_connect_object (priv->stack, "notify::visible-child", + G_CALLBACK (update_title_stack), self, G_CONNECT_SWAPPED); + +} diff --git a/src/shortcuts/dzl-shortcuts-window.h b/src/shortcuts/dzl-shortcuts-window.h new file mode 100644 index 0000000..e725f8e --- /dev/null +++ b/src/shortcuts/dzl-shortcuts-window.h @@ -0,0 +1,60 @@ +/* dzl-shortcuts-window.h + * + * Copyright (C) 2015 Christian Hergert + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#ifndef __DZL_SHORTCUTS_WINDOW_H__ +#define __DZL_SHORTCUTS_WINDOW_H__ + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SHORTCUTS_WINDOW (dzl_shortcuts_window_get_type ()) +#define DZL_SHORTCUTS_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), DZL_TYPE_SHORTCUTS_WINDOW, DzlShortcutsWindow)) +#define DZL_SHORTCUTS_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), DZL_TYPE_SHORTCUTS_WINDOW, DzlShortcutsWindowClass)) +#define DZL_IS_SHORTCUTS_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), DZL_TYPE_SHORTCUTS_WINDOW)) +#define DZL_IS_SHORTCUTS_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), DZL_TYPE_SHORTCUTS_WINDOW)) +#define DZL_SHORTCUTS_WINDOW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), DZL_TYPE_SHORTCUTS_WINDOW, DzlShortcutsWindowClass)) + + +typedef struct _DzlShortcutsWindow DzlShortcutsWindow; +typedef struct _DzlShortcutsWindowClass DzlShortcutsWindowClass; + + +struct _DzlShortcutsWindow +{ + GtkWindow window; +}; + +struct _DzlShortcutsWindowClass +{ + GtkWindowClass parent_class; + + void (*close) (DzlShortcutsWindow *self); + void (*search) (DzlShortcutsWindow *self); +}; + +DZL_AVAILABLE_IN_ALL +GType dzl_shortcuts_window_get_type (void) G_GNUC_CONST; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(DzlShortcutsWindow, g_object_unref) + +G_END_DECLS + +#endif /* DZL_SHORTCUTS_WINDOW _H */ diff --git a/src/shortcuts/enter-keyboard-shortcut.svg b/src/shortcuts/enter-keyboard-shortcut.svg new file mode 100644 index 0000000..b7ce2e4 --- /dev/null +++ b/src/shortcuts/enter-keyboard-shortcut.svg @@ -0,0 +1,245 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shortcuts/meson.build b/src/shortcuts/meson.build new file mode 100644 index 0000000..0df7fbb --- /dev/null +++ b/src/shortcuts/meson.build @@ -0,0 +1,43 @@ +shortcuts_headers = [ + 'dzl-shortcut-accel-dialog.h', + 'dzl-shortcut-chord.h', + 'dzl-shortcut-context.h', + 'dzl-shortcut-controller.h', + 'dzl-shortcut-label.h', + 'dzl-shortcut-manager.h', + 'dzl-shortcut-model.h', + 'dzl-shortcut-phase.h', + 'dzl-shortcut-simple-label.h', + 'dzl-shortcut-theme-editor.h', + 'dzl-shortcut-theme.h', + 'dzl-shortcuts-group.h', + 'dzl-shortcuts-section.h', + 'dzl-shortcuts-shortcut.h', + 'dzl-shortcuts-window.h', +] + +shortcuts_sources = [ + 'dzl-shortcut-accel-dialog.c', + 'dzl-shortcut-chord.c', + 'dzl-shortcut-context.c', + 'dzl-shortcut-controller.c', + 'dzl-shortcut-label.c', + 'dzl-shortcut-manager.c', + 'dzl-shortcut-model.c', + 'dzl-shortcut-phase.c', + 'dzl-shortcut-simple-label.c', + 'dzl-shortcut-theme-editor.c', + 'dzl-shortcut-theme-load.c', + 'dzl-shortcut-theme-save.c', + 'dzl-shortcut-theme.c', + 'dzl-shortcuts-group.c', + 'dzl-shortcuts-section.c', + 'dzl-shortcuts-shortcut.c', + 'dzl-shortcuts-window.c', +] + +libdazzle_public_headers += files(shortcuts_headers) +libdazzle_public_sources += files(shortcuts_sources) +libdazzle_private_sources += files('dzl-shortcut-closure-chain.c') + +install_headers(shortcuts_headers, subdir: join_paths(libdazzle_header_subdir, 'shortcuts')) diff --git a/src/statemachine/dzl-state-machine-buildable.c b/src/statemachine/dzl-state-machine-buildable.c new file mode 100644 index 0000000..a608ce7 --- /dev/null +++ b/src/statemachine/dzl-state-machine-buildable.c @@ -0,0 +1,689 @@ +/* dzl-state-machine-buildable.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-state-machine" + +#include "config.h" + +#include +#include + +#include "dzl-state-machine.h" +#include "dzl-state-machine-buildable.h" + +typedef struct +{ + DzlStateMachine *self; + GtkBuilder *builder; + GQueue *stack; +} StatesParserData; + +typedef enum +{ + STACK_ITEM_OBJECT, + STACK_ITEM_STATE, + STACK_ITEM_PROPERTY, +} StackItemType; + +typedef struct +{ + StackItemType type; + union { + struct { + gchar *id; + GSList *classes; + GSList *properties; + } object; + struct { + gchar *name; + GSList *objects; + } state; + struct { + gchar *name; + gchar *bind_source; + gchar *bind_property; + gchar *text; + GBindingFlags bind_flags; + } property; + } u; +} StackItem; + +static GtkBuildableIface *dzl_state_machine_parent_buildable; + +static void +stack_item_free (StackItem *item) +{ + switch (item->type) + { + case STACK_ITEM_OBJECT: + g_free (item->u.object.id); + g_slist_free_full (item->u.object.classes, g_free); + g_slist_free_full (item->u.object.properties, (GDestroyNotify)stack_item_free); + break; + + case STACK_ITEM_STATE: + g_free (item->u.state.name); + g_slist_free_full (item->u.state.objects, (GDestroyNotify)stack_item_free); + break; + + case STACK_ITEM_PROPERTY: + g_free (item->u.property.name); + g_free (item->u.property.bind_source); + g_free (item->u.property.bind_property); + g_free (item->u.property.text); + break; + + default: + g_assert_not_reached (); + break; + } + + g_slice_free (StackItem, item); +} + +static StackItem * +stack_item_new (StackItemType type) +{ + StackItem *item; + + item = g_slice_new0 (StackItem); + item->type = type; + + return item; +} + +static void +add_state (StatesParserData *parser_data, + StackItem *item, + GError **error) +{ + GSList *iter; + + g_assert (parser_data != NULL); + g_assert (item != NULL); + g_assert (item->type == STACK_ITEM_STATE); + + for (iter = item->u.state.objects; iter; iter = iter->next) + { + StackItem *stack_obj = iter->data; + GObject *object; + GSList *prop_iter; + GSList *style_iter; + + g_assert (stack_obj->type == STACK_ITEM_OBJECT); + g_assert (stack_obj->u.object.id != NULL); + + object = gtk_builder_get_object (parser_data->builder, stack_obj->u.object.id); + + if (object == NULL) + { + g_set_error (error, + GTK_BUILDER_ERROR, + GTK_BUILDER_ERROR_INVALID_VALUE, + "Unknown object for state '%s': %s", + item->u.state.name, + stack_obj->u.object.id); + return; + } + + if (GTK_IS_WIDGET (object)) + for (style_iter = stack_obj->u.object.classes; style_iter; style_iter = style_iter->next) + dzl_state_machine_add_style (parser_data->self, + item->u.state.name, + GTK_WIDGET (object), + style_iter->data); + + for (prop_iter = stack_obj->u.object.properties; prop_iter; prop_iter = prop_iter->next) + { + StackItem *stack_prop = prop_iter->data; + GObject *bind_source; + + g_assert (stack_prop->type == STACK_ITEM_PROPERTY); + + if ((stack_prop->u.property.bind_source != NULL) && + (stack_prop->u.property.bind_property != NULL) && + (bind_source = gtk_builder_get_object (parser_data->builder, stack_prop->u.property.bind_source))) + { + dzl_state_machine_add_binding (parser_data->self, + item->u.state.name, + bind_source, + stack_prop->u.property.bind_property, + object, + stack_prop->u.property.name, + stack_prop->u.property.bind_flags); + } + else if (stack_prop->u.property.text != NULL) + { + GParamSpec *pspec; + GValue value = G_VALUE_INIT; + + pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (object), stack_prop->u.property.name); + + if (pspec == NULL) + { + g_set_error (error, + GTK_BUILDER_ERROR, + GTK_BUILDER_ERROR_INVALID_PROPERTY, + "No such property: %s", + stack_prop->u.property.name); + return; + } + + if (g_type_is_a (pspec->value_type, G_TYPE_OBJECT)) + { + GObject *relative; + + relative = gtk_builder_get_object (parser_data->builder, stack_prop->u.property.text); + + if (relative == NULL) + { + g_set_error (error, + GTK_BUILDER_ERROR, + GTK_BUILDER_ERROR_INVALID_VALUE, + "Unknown object for property '%s': %s", + stack_prop->u.property.name, + stack_prop->u.property.text); + return; + } + + g_value_init (&value, pspec->value_type); + g_value_set_object (&value, relative); + } + else if (!gtk_builder_value_from_string (parser_data->builder, + pspec, + stack_prop->u.property.text, + &value, + error)) + { + return; + } + + dzl_state_machine_add_propertyv (parser_data->self, + item->u.state.name, + object, + stack_prop->u.property.name, + &value); + + g_value_unset (&value); + } + } + } +} + +static void +add_object (StatesParserData *parser_data, + StackItem *parent, + StackItem *item) +{ + g_assert (parser_data != NULL); + g_assert (parent != NULL); + g_assert (parent->type == STACK_ITEM_STATE); + g_assert (item != NULL); + g_assert (item->type == STACK_ITEM_OBJECT); + + parent->u.state.objects = g_slist_prepend (parent->u.state.objects, item); +} + +static void +add_property (StatesParserData *parser_data, + StackItem *parent, + StackItem *item) +{ + g_assert (parser_data != NULL); + g_assert (parent != NULL); + g_assert (parent->type == STACK_ITEM_OBJECT); + g_assert (item != NULL); + g_assert (item->type == STACK_ITEM_PROPERTY); + + parent->u.object.properties = g_slist_prepend (parent->u.object.properties, item); +} + +static gboolean +check_parent (GMarkupParseContext *context, + const gchar *element_name, + GError **error) +{ + const GSList *stack; + const gchar *parent_name; + const gchar *our_name; + + stack = g_markup_parse_context_get_element_stack (context); + our_name = stack->data; + parent_name = stack->next ? stack->next->data : ""; + + if (g_strcmp0 (parent_name, element_name) != 0) + { + gint line; + gint col; + + g_markup_parse_context_get_position (context, &line, &col); + g_set_error (error, + GTK_BUILDER_ERROR, + GTK_BUILDER_ERROR_INVALID_TAG, + "%d:%d: Element <%s> found in <%s>, expected <%s>.", + line, col, our_name, parent_name, element_name); + return FALSE; + } + + return TRUE; +} + +/* + * flags_from_string: + * + * gtkbuilder.c + * + * Copyright (C) 1998-2002 James Henstridge + * Copyright (C) 2006-2007 Async Open Source, + * Johan Dahlin , + * Henrique Romano + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ +gboolean +flags_from_string (GType type, + const gchar *string, + guint *flags_value, + GError **error) +{ + GFlagsClass *fclass; + gchar *endptr, *prevptr; + guint i, j, value; + gchar *flagstr; + GFlagsValue *fv; + const gchar *flag; + gunichar ch; + gboolean eos, ret; + + g_return_val_if_fail (G_TYPE_IS_FLAGS (type), FALSE); + g_return_val_if_fail (string != 0, FALSE); + + ret = TRUE; + + endptr = NULL; + errno = 0; + value = g_ascii_strtoull (string, &endptr, 0); + if (errno == 0 && endptr != string) /* parsed a number */ + *flags_value = value; + else + { + fclass = g_type_class_ref (type); + + flagstr = g_strdup (string); + for (value = i = j = 0; ; i++) + { + + eos = flagstr[i] == '\0'; + + if (!eos && flagstr[i] != '|') + continue; + + flag = &flagstr[j]; + endptr = &flagstr[i]; + + if (!eos) + { + flagstr[i++] = '\0'; + j = i; + } + + /* trim spaces */ + for (;;) + { + ch = g_utf8_get_char (flag); + if (!g_unichar_isspace (ch)) + break; + flag = g_utf8_next_char (flag); + } + + while (endptr > flag) + { + prevptr = g_utf8_prev_char (endptr); + ch = g_utf8_get_char (prevptr); + if (!g_unichar_isspace (ch)) + break; + endptr = prevptr; + } + + if (endptr > flag) + { + *endptr = '\0'; + fv = g_flags_get_value_by_name (fclass, flag); + + if (!fv) + fv = g_flags_get_value_by_nick (fclass, flag); + + if (fv) + value |= fv->value; + else + { + g_set_error (error, + GTK_BUILDER_ERROR, + GTK_BUILDER_ERROR_INVALID_VALUE, + "Unknown flag: `%s'", + flag); + ret = FALSE; + break; + } + } + + if (eos) + { + *flags_value = value; + break; + } + } + + g_free (flagstr); + + g_type_class_unref (fclass); + } + + return ret; +} + +static void +states_parser_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + StatesParserData *parser_data = user_data; + StackItem *item; + + g_assert (context != NULL); + g_assert (element_name != NULL); + g_assert (parser_data != NULL); + + if (g_strcmp0 (element_name, "state") == 0) + { + const gchar *name; + + if (!check_parent (context, "states", error)) + return; + + if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error, + G_MARKUP_COLLECT_STRING, "name", &name, + G_MARKUP_COLLECT_INVALID)) + return; + + item = stack_item_new (STACK_ITEM_STATE); + item->u.state.name = g_strdup (name); + g_queue_push_head (parser_data->stack, item); + } + else if (g_strcmp0 (element_name, "states") == 0) + { + if (!check_parent (context, "object", error)) + return; + } + else if (g_strcmp0 (element_name, "object") == 0) + { + const gchar *id; + + if (!check_parent (context, "state", error)) + return; + + if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error, + G_MARKUP_COLLECT_STRING, "id", &id, + G_MARKUP_COLLECT_INVALID)) + return; + + item = stack_item_new (STACK_ITEM_OBJECT); + item->u.object.id = g_strdup (id); + g_queue_push_head (parser_data->stack, item); + } + else if (g_strcmp0 (element_name, "property") == 0) + { + const gchar *name = NULL; + const gchar *translatable = NULL; + const gchar *bind_source = NULL; + const gchar *bind_property = NULL; + const gchar *bind_flags_str = NULL; + GBindingFlags bind_flags = 0; + + if (!check_parent (context, "object", error)) + return; + + if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error, + G_MARKUP_COLLECT_STRING, "name", &name, + G_MARKUP_COLLECT_STRING|G_MARKUP_COLLECT_OPTIONAL, "translatable", &translatable, + G_MARKUP_COLLECT_STRING|G_MARKUP_COLLECT_OPTIONAL, "bind-source", &bind_source, + G_MARKUP_COLLECT_STRING|G_MARKUP_COLLECT_OPTIONAL, "bind-property", &bind_property, + G_MARKUP_COLLECT_STRING|G_MARKUP_COLLECT_OPTIONAL, "bind-flags", &bind_flags_str, + G_MARKUP_COLLECT_INVALID)) + return; + + if (name != NULL) + { + if (g_strcmp0 (translatable, "yes") == 0) + { + const gchar *domain; + + domain = gtk_builder_get_translation_domain (parser_data->builder); + name = dgettext (domain, name); + } + } + + if ((bind_flags_str != NULL) && !flags_from_string (G_TYPE_BINDING_FLAGS, bind_flags_str, &bind_flags, error)) + return; + + item = stack_item_new (STACK_ITEM_PROPERTY); + item->u.property.name = g_strdup (name); + item->u.property.bind_source = g_strdup (bind_source); + item->u.property.bind_property = g_strdup (bind_property); + item->u.property.bind_flags = bind_flags; + g_queue_push_head (parser_data->stack, item); + } + else if (g_strcmp0 (element_name, "style") == 0) + { + if (!check_parent (context, "object", error)) + return; + } + else if (g_strcmp0 (element_name, "class") == 0) + { + const gchar *name = NULL; + + if (!check_parent (context, "style", error)) + return; + + if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error, + G_MARKUP_COLLECT_STRING, "name", &name, + G_MARKUP_COLLECT_INVALID)) + return; + + item = g_queue_peek_head (parser_data->stack); + g_assert (item->type == STACK_ITEM_OBJECT); + + item->u.object.classes = g_slist_prepend (item->u.object.classes, g_strdup (name)); + } + else + { + const GSList *stack; + const gchar *parent_name; + const gchar *our_name; + gint line; + gint col; + + stack = g_markup_parse_context_get_element_stack (context); + our_name = stack->data; + parent_name = stack->next ? stack->next->data : ""; + + g_markup_parse_context_get_position (context, &line, &col); + g_set_error (error, + GTK_BUILDER_ERROR, + GTK_BUILDER_ERROR_INVALID_TAG, + "%d:%d: Unknown element <%s> found in <%s>.", + line, col, our_name, parent_name); + } + + return; +} + +static void +states_parser_end_element (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + StatesParserData *parser_data = user_data; + StackItem *item; + + g_assert (context != NULL); + g_assert (element_name != NULL); + g_assert (parser_data != NULL); + + if (g_strcmp0 (element_name, "state") == 0) + { + item = g_queue_pop_head (parser_data->stack); + g_assert (item->type == STACK_ITEM_STATE); + add_state (parser_data, item, error); + stack_item_free (item); + } + else if (g_strcmp0 (element_name, "object") == 0) + { + StackItem *parent; + + item = g_queue_pop_head (parser_data->stack); + g_assert (item->type == STACK_ITEM_OBJECT); + + parent = g_queue_peek_head (parser_data->stack); + g_assert (parent->type == STACK_ITEM_STATE); + + add_object (parser_data, parent, item); + } + else if (g_strcmp0 (element_name, "property") == 0) + { + StackItem *parent; + + item = g_queue_pop_head (parser_data->stack); + g_assert (item->type == STACK_ITEM_PROPERTY); + + parent = g_queue_peek_head (parser_data->stack); + g_assert (parent->type == STACK_ITEM_OBJECT); + + add_property (parser_data, parent, item); + } +} + +static void +states_parser_text (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + StatesParserData *parser_data = user_data; + StackItem *item; + + g_assert (parser_data != NULL); + + item = g_queue_peek_head (parser_data->stack); + if ((item != NULL) && (item->type == STACK_ITEM_PROPERTY)) + item->u.property.text = g_strndup (text, text_len); +} + +static GMarkupParser StatesParser = { + states_parser_start_element, + states_parser_end_element, + states_parser_text, +}; + +static gboolean +dzl_state_machine_buildable_custom_tag_start (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *tagname, + GMarkupParser *parser, + gpointer *data) +{ + DzlStateMachine *self = (DzlStateMachine *)buildable; + + g_assert (DZL_IS_STATE_MACHINE (self)); + g_assert (GTK_IS_BUILDER (builder)); + g_assert (tagname != NULL); + g_assert (parser != NULL); + g_assert (data != NULL); + + if (g_strcmp0 (tagname, "states") == 0) + { + StatesParserData *parser_data; + + parser_data = g_slice_new0 (StatesParserData); + parser_data->self = g_object_ref (DZL_STATE_MACHINE (buildable)); + parser_data->builder = g_object_ref (builder); + parser_data->stack = g_queue_new (); + + *parser = StatesParser; + *data = parser_data; + + return TRUE; + } + + return FALSE; +} + +static void +dzl_state_machine_buildable_custom_finished (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *tagname, + gpointer user_data) +{ + DzlStateMachine *self = (DzlStateMachine *)buildable; + + g_assert (DZL_IS_STATE_MACHINE (self)); + g_assert (GTK_IS_BUILDER (builder)); + g_assert (tagname != NULL); + + if (g_strcmp0 (tagname, "states") == 0) + { + StatesParserData *parser_data = user_data; + + g_object_unref (parser_data->self); + g_object_unref (parser_data->builder); + g_queue_free_full (parser_data->stack, (GDestroyNotify)stack_item_free); + g_slice_free (StatesParserData, parser_data); + } +} + +/** + * dzl_state_machine_buildable_iface_init: (skip) + */ +void +dzl_state_machine_buildable_iface_init (GtkBuildableIface *iface) +{ + g_assert (iface != NULL); + + dzl_state_machine_parent_buildable = g_type_interface_peek_parent (iface); + + iface->custom_tag_start = dzl_state_machine_buildable_custom_tag_start; + iface->custom_finished = dzl_state_machine_buildable_custom_finished; +} diff --git a/src/statemachine/dzl-state-machine-buildable.h b/src/statemachine/dzl-state-machine-buildable.h new file mode 100644 index 0000000..49e5063 --- /dev/null +++ b/src/statemachine/dzl-state-machine-buildable.h @@ -0,0 +1,33 @@ +/* dzl-state-machine-buildable.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_STATE_MACHINE_BUILDABLE_H +#define DZL_STATE_MACHINE_BUILDABLE_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +void dzl_state_machine_buildable_iface_init (GtkBuildableIface *iface); + +G_END_DECLS + +#endif /* DZL_STATE_MACHINE_BUILDABLE_H */ diff --git a/src/statemachine/dzl-state-machine.c b/src/statemachine/dzl-state-machine.c new file mode 100644 index 0000000..8762c5d --- /dev/null +++ b/src/statemachine/dzl-state-machine.c @@ -0,0 +1,770 @@ +/* dzl-state-machine.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-state-machine" + +#include "config.h" + +#include +#include + +#include "bindings/dzl-binding-group.h" +#include "bindings/dzl-signal-group.h" + +#include "statemachine/dzl-state-machine.h" +#include "statemachine/dzl-state-machine-buildable.h" + +G_DEFINE_QUARK (dzl_state_machine_error, dzl_state_machine_error) + +typedef struct +{ + gchar *state; + GHashTable *states; +} DzlStateMachinePrivate; + +typedef struct +{ + gchar *name; + GHashTable *signals; + GHashTable *bindings; + GPtrArray *properties; + GPtrArray *styles; +} DzlState; + +typedef struct +{ + DzlStateMachine *state_machine; + gpointer object; + gchar *property; + GValue value; +} DzlStateProperty; + +typedef struct +{ + DzlStateMachine *state_machine; + GtkWidget *widget; + gchar *name; +} DzlStateStyle; + +G_DEFINE_TYPE_WITH_CODE (DzlStateMachine, dzl_state_machine, G_TYPE_OBJECT, + G_ADD_PRIVATE (DzlStateMachine) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, + dzl_state_machine_buildable_iface_init)) + +enum { + PROP_0, + PROP_STATE, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +static void +dzl_state_machine__property_object_weak_notify (gpointer data, + GObject *where_object_was) +{ + DzlStateProperty *state_prop = data; + DzlStateMachine *self = state_prop->state_machine; + DzlStateMachinePrivate *priv = dzl_state_machine_get_instance_private (self); + GHashTableIter iter; + DzlState *state; + + g_assert (DZL_IS_STATE_MACHINE (self)); + g_assert (where_object_was != NULL); + + state_prop->object = NULL; + + g_hash_table_iter_init (&iter, priv->states); + while (g_hash_table_iter_next (&iter, NULL, (gpointer)&state)) + { + if (g_ptr_array_remove_fast (state->properties, state_prop)) + return; + } + + g_critical ("Failed to find property for %p", where_object_was); +} + +static void +dzl_state_machine__style_object_weak_notify (gpointer data, + GObject *where_object_was) +{ + DzlStateStyle *style_prop = data; + DzlStateMachine *self = style_prop->state_machine; + DzlStateMachinePrivate *priv = dzl_state_machine_get_instance_private (self); + GHashTableIter iter; + DzlState *state; + + g_assert (DZL_IS_STATE_MACHINE (self)); + g_assert (where_object_was != NULL); + + style_prop->widget = NULL; + + g_hash_table_iter_init (&iter, priv->states); + while (g_hash_table_iter_next (&iter, NULL, (gpointer)&state)) + { + if (g_ptr_array_remove_fast (state->styles, style_prop)) + return; + } + + g_critical ("Failed to find style for %p", where_object_was); +} + +static void +dzl_state_machine__binding_source_weak_notify (gpointer data, + GObject *where_object_was) +{ + DzlStateMachine *self = data; + DzlStateMachinePrivate *priv = dzl_state_machine_get_instance_private (self); + GHashTableIter iter; + DzlState *state; + + g_assert (DZL_IS_STATE_MACHINE (self)); + g_assert (where_object_was != NULL); + + g_hash_table_iter_init (&iter, priv->states); + while (g_hash_table_iter_next (&iter, NULL, (gpointer)&state)) + { + DzlBindingGroup *bindings; + + bindings = g_hash_table_lookup (state->bindings, where_object_was); + + if (bindings != NULL) + { + g_hash_table_remove (state->bindings, where_object_was); + return; + } + } + + g_critical ("Failed to find bindings for %p", where_object_was); +} + +static void +dzl_state_machine__signal_source_weak_notify (gpointer data, + GObject *where_object_was) +{ + DzlStateMachine *self = data; + DzlStateMachinePrivate *priv = dzl_state_machine_get_instance_private (self); + GHashTableIter iter; + DzlState *state; + + g_assert (DZL_IS_STATE_MACHINE (self)); + g_assert (where_object_was != NULL); + + g_hash_table_iter_init (&iter, priv->states); + while (g_hash_table_iter_next (&iter, NULL, (gpointer)&state)) + { + DzlSignalGroup *signals; + + signals = g_hash_table_lookup (state->signals, where_object_was); + + if (signals != NULL) + { + g_hash_table_remove (state->signals, where_object_was); + return; + } + } + + g_critical ("Failed to find signals for %p", where_object_was); +} + +static void +dzl_state_free (gpointer data) +{ + DzlState *state = data; + + g_free (state->name); + g_hash_table_unref (state->signals); + g_hash_table_unref (state->bindings); + g_ptr_array_unref (state->properties); + g_ptr_array_unref (state->styles); + g_slice_free (DzlState, state); +} + +static void +dzl_state_property_free (gpointer data) +{ + DzlStateProperty *prop = data; + + if (prop->object != NULL) + { + g_object_weak_unref (prop->object, + dzl_state_machine__property_object_weak_notify, + prop); + prop->object = NULL; + } + + g_free (prop->property); + g_value_unset (&prop->value); + g_slice_free (DzlStateProperty, prop); +} + +static void +dzl_state_style_free (gpointer data) +{ + DzlStateStyle *style = data; + + if (style->widget != NULL) + { + g_object_weak_unref (G_OBJECT (style->widget), + dzl_state_machine__style_object_weak_notify, + style); + style->widget = NULL; + } + + g_free (style->name); + g_slice_free (DzlStateStyle, style); +} + +static void +dzl_state_apply (DzlStateMachine *self, + DzlState *state) +{ + GHashTableIter iter; + gpointer key; + gpointer value; + gsize i; + + g_assert (DZL_IS_STATE_MACHINE (self)); + g_assert (state != NULL); + + g_hash_table_iter_init (&iter, state->bindings); + while (g_hash_table_iter_next (&iter, &key, &value)) + dzl_binding_group_set_source (value, key); + + g_hash_table_iter_init (&iter, state->signals); + while (g_hash_table_iter_next (&iter, &key, &value)) + dzl_signal_group_set_target (value, key); + + for (i = 0; i < state->properties->len; i++) + { + DzlStateProperty *prop; + + prop = g_ptr_array_index (state->properties, i); + g_object_set_property (prop->object, prop->property, &prop->value); + } + + for (i = 0; i < state->styles->len; i++) + { + DzlStateStyle *style; + GtkStyleContext *style_context; + + style = g_ptr_array_index (state->styles, i); + style_context = gtk_widget_get_style_context (GTK_WIDGET (style->widget)); + gtk_style_context_add_class (style_context, style->name); + } +} + +static void +dzl_state_unapply (DzlStateMachine *self, + DzlState *state) +{ + GHashTableIter iter; + gpointer key; + gpointer value; + gsize i; + + g_assert (DZL_IS_STATE_MACHINE (self)); + g_assert (state != NULL); + + g_hash_table_iter_init (&iter, state->bindings); + while (g_hash_table_iter_next (&iter, &key, &value)) + dzl_binding_group_set_source (value, NULL); + + g_hash_table_iter_init (&iter, state->signals); + while (g_hash_table_iter_next (&iter, &key, &value)) + dzl_signal_group_set_target (value, NULL); + + for (i = 0; i < state->styles->len; i++) + { + DzlStateStyle *style; + GtkStyleContext *style_context; + + style = g_ptr_array_index (state->styles, i); + style_context = gtk_widget_get_style_context (GTK_WIDGET (style->widget)); + gtk_style_context_remove_class (style_context, style->name); + } +} + +static DzlState * +dzl_state_machine_get_state_obj (DzlStateMachine *self, + const gchar *state) +{ + DzlStateMachinePrivate *priv = dzl_state_machine_get_instance_private (self); + DzlState *state_obj; + + g_assert (DZL_IS_STATE_MACHINE (self)); + + state_obj = g_hash_table_lookup (priv->states, state); + + if (state_obj == NULL) + { + state_obj = g_slice_new0 (DzlState); + state_obj->name = g_strdup (state); + state_obj->signals = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_object_unref); + state_obj->bindings = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_object_unref); + state_obj->properties = g_ptr_array_new_with_free_func (dzl_state_property_free); + state_obj->styles = g_ptr_array_new_with_free_func (dzl_state_style_free); + g_hash_table_insert (priv->states, g_strdup (state), state_obj); + } + + return state_obj; +} + +static void +dzl_state_machine_transition (DzlStateMachine *self, + const gchar *old_state, + const gchar *new_state) +{ + DzlState *state_obj; + + g_assert (DZL_IS_STATE_MACHINE (self)); + + g_object_freeze_notify (G_OBJECT (self)); + + if (old_state && (state_obj = dzl_state_machine_get_state_obj (self, old_state))) + dzl_state_unapply (self, state_obj); + + if (new_state && (state_obj = dzl_state_machine_get_state_obj (self, new_state))) + dzl_state_apply (self, state_obj); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STATE]); + + g_object_thaw_notify (G_OBJECT (self)); +} + +static void +dzl_state_machine_finalize (GObject *object) +{ + DzlStateMachine *self = (DzlStateMachine *)object; + DzlStateMachinePrivate *priv = dzl_state_machine_get_instance_private (self); + GHashTableIter state_iter; + DzlState *state; + + g_hash_table_iter_init (&state_iter, priv->states); + while (g_hash_table_iter_next (&state_iter, NULL, (gpointer)&state)) + { + GHashTableIter iter; + gpointer key; + + g_hash_table_iter_init (&iter, state->bindings); + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + g_object_weak_unref (key, + dzl_state_machine__binding_source_weak_notify, + self); + } + + g_hash_table_iter_init (&iter, state->signals); + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + g_object_weak_unref (key, + dzl_state_machine__signal_source_weak_notify, + self); + } + } + + g_clear_pointer (&priv->states, g_hash_table_unref); + g_clear_pointer (&priv->state, g_free); + + G_OBJECT_CLASS (dzl_state_machine_parent_class)->finalize (object); +} + +static void +dzl_state_machine_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlStateMachine *self = DZL_STATE_MACHINE (object); + + switch (prop_id) + { + case PROP_STATE: + g_value_set_string (value, dzl_state_machine_get_state (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_state_machine_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlStateMachine *self = DZL_STATE_MACHINE (object); + + switch (prop_id) + { + case PROP_STATE: + dzl_state_machine_set_state (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_state_machine_class_init (DzlStateMachineClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_state_machine_finalize; + object_class->get_property = dzl_state_machine_get_property; + object_class->set_property = dzl_state_machine_set_property; + + properties [PROP_STATE] = + g_param_spec_string ("state", + "State", + "The current state of the machine.", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_state_machine_init (DzlStateMachine *self) +{ + DzlStateMachinePrivate *priv = dzl_state_machine_get_instance_private (self); + + priv->states = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, dzl_state_free); +} + +DzlStateMachine * +dzl_state_machine_new (void) +{ + return g_object_new (DZL_TYPE_STATE_MACHINE, NULL); +} + +/** + * dzl_state_machine_get_state: + * @self: the #DzlStateMachine. + * + * Gets the #DzlStateMachine:state property. This is the name of the + * current state of the machine. + * + * Returns: The current state of the machine. + */ +const gchar * +dzl_state_machine_get_state (DzlStateMachine *self) +{ + DzlStateMachinePrivate *priv = dzl_state_machine_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_STATE_MACHINE (self), NULL); + + return priv->state; +} + +/** + * dzl_state_machine_set_state: + * @self: the #DzlStateMachine @self: the # + * + * Sets the #DzlStateMachine:state property. + * + * Registered state transformations will be applied during the state + * transformation. + * + * If the transition results in a cyclic operation, the state will stop at + * the last state before the cycle was detected. + */ +void +dzl_state_machine_set_state (DzlStateMachine *self, + const gchar *state) +{ + DzlStateMachinePrivate *priv = dzl_state_machine_get_instance_private (self); + + g_return_if_fail (DZL_IS_STATE_MACHINE (self)); + + if (g_strcmp0 (priv->state, state) != 0) + { + gchar *old_state = priv->state; + gchar *new_state = g_strdup (state); + + /* + * Steal ownership of old state and create a copy for new state + * to ensure that we own the references. State machines tend to + * get used in re-entrant fashion. + */ + + priv->state = g_strdup (state); + + dzl_state_machine_transition (self, old_state, state); + + g_free (new_state); + g_free (old_state); + } +} + +/** + * dzl_state_machine_create_action: + * @self: An #DzlStateMachine + * @name: the name of the action. + * + * Creates a new #GAction with the name of @name. + * + * Setting the state of this action will toggle the state of the state machine. + * You should use g_variant_new_string() or similar to create the state. + * + * Returns: (transfer full): A newly created #GAction. + */ +GAction * +dzl_state_machine_create_action (DzlStateMachine *self, + const gchar *name) +{ + g_return_val_if_fail (DZL_IS_STATE_MACHINE (self), NULL); + g_return_val_if_fail (name != NULL, NULL); + + return G_ACTION (g_property_action_new (name, self, "state")); +} + +void +dzl_state_machine_add_property (DzlStateMachine *self, + const gchar *state, + gpointer object, + const gchar *property, + ...) +{ + va_list var_args; + + g_return_if_fail (DZL_IS_STATE_MACHINE (self)); + g_return_if_fail (state != NULL); + g_return_if_fail (object != NULL); + g_return_if_fail (property != NULL); + + va_start (var_args, property); + dzl_state_machine_add_property_valist (self, state, object, + property, var_args); + va_end (var_args); +} + +void +dzl_state_machine_add_property_valist (DzlStateMachine *self, + const gchar *state, + gpointer object, + const gchar *property, + va_list var_args) +{ + GParamSpec *pspec; + gchar *error = NULL; + GValue value = G_VALUE_INIT; + + g_return_if_fail (DZL_IS_STATE_MACHINE (self)); + g_return_if_fail (state != NULL); + g_return_if_fail (object != NULL); + g_return_if_fail (property != NULL); + + pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (object), + property); + g_return_if_fail (pspec != NULL); + + G_VALUE_COLLECT_INIT (&value, pspec->value_type, var_args, 0, &error); + + if (error != NULL) + { + g_critical ("%s: %s", G_STRFUNC, error); + g_free (error); + } + else + { + dzl_state_machine_add_propertyv (self, state, object, + property, &value); + } + + g_value_unset (&value); +} + +void +dzl_state_machine_add_propertyv (DzlStateMachine *self, + const gchar *state, + gpointer object, + const gchar *property, + const GValue *value) +{ + DzlStateMachinePrivate *priv = dzl_state_machine_get_instance_private (self); + DzlState *state_obj; + DzlStateProperty *state_prop; + + g_return_if_fail (DZL_IS_STATE_MACHINE (self)); + g_return_if_fail (state != NULL); + g_return_if_fail (G_IS_OBJECT (object)); + g_return_if_fail (property != NULL); + g_return_if_fail (G_IS_VALUE (value)); + + state_obj = dzl_state_machine_get_state_obj (self, state); + + state_prop = g_slice_new0 (DzlStateProperty); + state_prop->state_machine = self; + state_prop->object = object; + state_prop->property = g_strdup (property); + g_value_init (&state_prop->value, G_VALUE_TYPE (value)); + g_value_copy (value, &state_prop->value); + + g_object_weak_ref (object, + dzl_state_machine__property_object_weak_notify, + state_prop); + + g_ptr_array_add (state_obj->properties, state_prop); + + if (g_strcmp0 (state, priv->state) == 0) + g_object_set_property (object, property, value); +} + +void +dzl_state_machine_add_binding (DzlStateMachine *self, + const gchar *state, + gpointer source_object, + const gchar *source_property, + gpointer target_object, + const gchar *target_property, + GBindingFlags flags) +{ + DzlBindingGroup *bindings; + DzlState *state_obj; + + g_return_if_fail (DZL_IS_STATE_MACHINE (self)); + g_return_if_fail (state != NULL); + g_return_if_fail (G_IS_OBJECT (source_object)); + g_return_if_fail (source_property != NULL); + g_return_if_fail (G_IS_OBJECT (target_object)); + g_return_if_fail (target_property != NULL); + + state_obj = dzl_state_machine_get_state_obj (self, state); + + bindings = g_hash_table_lookup (state_obj->bindings, source_object); + + if (bindings == NULL) + { + bindings = dzl_binding_group_new (); + g_hash_table_insert (state_obj->bindings, source_object, bindings); + + g_object_weak_ref (source_object, + dzl_state_machine__binding_source_weak_notify, + self); + } + + dzl_binding_group_bind (bindings, source_property, target_object, target_property, flags); +} + +void +dzl_state_machine_add_style (DzlStateMachine *self, + const gchar *state, + GtkWidget *widget, + const gchar *style) +{ + DzlStateMachinePrivate *priv = dzl_state_machine_get_instance_private (self); + DzlState *state_obj; + DzlStateStyle *style_obj; + + g_return_if_fail (DZL_IS_STATE_MACHINE (self)); + g_return_if_fail (state != NULL); + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (style != NULL); + + state_obj = dzl_state_machine_get_state_obj (self, state); + + style_obj = g_slice_new0 (DzlStateStyle); + style_obj->state_machine = self; + style_obj->name = g_strdup (style); + style_obj->widget = widget; + + g_object_weak_ref (G_OBJECT (widget), + dzl_state_machine__style_object_weak_notify, + style_obj); + + g_ptr_array_add (state_obj->styles, style_obj); + + if (g_strcmp0 (state, priv->state) == 0) + { + GtkStyleContext *style_context; + + style_context = gtk_widget_get_style_context (widget); + gtk_style_context_add_class (style_context, style); + } +} + +/** + * dzl_state_machine_connect_object: (skip) + * @self: A #DzlStateMachine. + * @state: The state the signal connection should exist within + * @source: the source object to connect to + * @detailed_signal: The detailed signal of @source to connect. + * @callback: (scope notified) (closure user_data): The callback to execute upon signal emission. + * @user_data: The user data for @callback. + * @flags: signal connection flags. + * + * Connects to the @detailed_signal of @source only when the current + * state of the state machine is @state. + */ +void +dzl_state_machine_connect_object (DzlStateMachine *self, + const gchar *state, + gpointer source, + const gchar *detailed_signal, + GCallback callback, + gpointer user_data, + GConnectFlags flags) +{ + DzlState *state_obj; + DzlSignalGroup *signals; + + g_return_if_fail (DZL_IS_STATE_MACHINE (self)); + g_return_if_fail (state != NULL); + g_return_if_fail (G_IS_OBJECT (source)); + g_return_if_fail (detailed_signal != NULL); + g_return_if_fail (callback != NULL); + + state_obj = dzl_state_machine_get_state_obj (self, state); + + if (!(signals = g_hash_table_lookup (state_obj->signals, source))) + { + signals = dzl_signal_group_new (G_OBJECT_TYPE (source)); + g_hash_table_insert (state_obj->signals, source, signals); + + g_object_weak_ref (source, + dzl_state_machine__signal_source_weak_notify, + self); + } + + dzl_signal_group_connect_object (signals, detailed_signal, callback, user_data, flags); +} + +/** + * dzl_state_machine_is_state: + * @self: a #DzlStateMachine + * @state: (nullable): the name of the state to check + * + * Checks to see if the current state of the #DzlStateMachine matches @state. + * + * Returns: %TRUE if @self is currently set to @state. + * + * Since: 3.28 + */ +gboolean +dzl_state_machine_is_state (DzlStateMachine *self, + const gchar *state) +{ + DzlStateMachinePrivate *priv = dzl_state_machine_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_STATE_MACHINE (self), FALSE); + + return g_strcmp0 (priv->state, state) == 0; +} diff --git a/src/statemachine/dzl-state-machine.h b/src/statemachine/dzl-state-machine.h new file mode 100644 index 0000000..43c513a --- /dev/null +++ b/src/statemachine/dzl-state-machine.h @@ -0,0 +1,94 @@ +/* dzl-state-machine.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_STATE_MACHINE_H +#define DZL_STATE_MACHINE_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_STATE_MACHINE (dzl_state_machine_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlStateMachine, dzl_state_machine, DZL, STATE_MACHINE, GObject) + +struct _DzlStateMachineClass +{ + GObjectClass parent; +}; + +DZL_AVAILABLE_IN_ALL +DzlStateMachine *dzl_state_machine_new (void); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_state_machine_get_state (DzlStateMachine *self); +DZL_AVAILABLE_IN_ALL +void dzl_state_machine_set_state (DzlStateMachine *self, + const gchar *state); +DZL_AVAILABLE_IN_3_28 +gboolean dzl_state_machine_is_state (DzlStateMachine *self, + const gchar *state); +DZL_AVAILABLE_IN_ALL +GAction *dzl_state_machine_create_action (DzlStateMachine *self, + const gchar *name); +DZL_AVAILABLE_IN_ALL +void dzl_state_machine_add_property (DzlStateMachine *self, + const gchar *state, + gpointer object, + const gchar *property, + ...); +DZL_AVAILABLE_IN_ALL +void dzl_state_machine_add_property_valist + (DzlStateMachine *self, + const gchar *state, + gpointer object, + const gchar *property, + va_list var_args); +DZL_AVAILABLE_IN_ALL +void dzl_state_machine_add_propertyv (DzlStateMachine *self, + const gchar *state, + gpointer object, + const gchar *property, + const GValue *value); +DZL_AVAILABLE_IN_ALL +void dzl_state_machine_add_binding (DzlStateMachine *self, + const gchar *state, + gpointer source_object, + const gchar *source_property, + gpointer target_object, + const gchar *target_property, + GBindingFlags flags); +DZL_AVAILABLE_IN_ALL +void dzl_state_machine_add_style (DzlStateMachine *self, + const gchar *state, + GtkWidget *widget, + const gchar *style); +DZL_AVAILABLE_IN_ALL +void dzl_state_machine_connect_object (DzlStateMachine *self, + const gchar *state, + gpointer source, + const gchar *detailed_signal, + GCallback callback, + gpointer user_data, + GConnectFlags flags); + +G_END_DECLS + +#endif /* DZL_STATE_MACHINE_H */ diff --git a/src/statemachine/meson.build b/src/statemachine/meson.build new file mode 100644 index 0000000..546c9ef --- /dev/null +++ b/src/statemachine/meson.build @@ -0,0 +1,14 @@ +statemachine_headers = [ + 'dzl-state-machine-buildable.h', + 'dzl-state-machine.h', +] + +statemachine_sources = [ + 'dzl-state-machine-buildable.c', + 'dzl-state-machine.c', +] + +libdazzle_public_headers += files(statemachine_headers) +libdazzle_public_sources += files(statemachine_sources) + +install_headers(statemachine_headers, subdir: join_paths(libdazzle_header_subdir, 'statemachine')) diff --git a/src/suggestions/dzl-suggestion-entry-buffer.c b/src/suggestions/dzl-suggestion-entry-buffer.c new file mode 100644 index 0000000..8b3e84b --- /dev/null +++ b/src/suggestions/dzl-suggestion-entry-buffer.c @@ -0,0 +1,410 @@ +/* dzl-suggestion-entry-buffer.c + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-suggestion-entry-buffer" + +#include "config.h" + +#include + +#include "dzl-suggestion-entry-buffer.h" + +typedef struct +{ + DzlSuggestion *suggestion; + gchar *text; + gchar *suffix; + guint in_insert : 1; + guint in_delete : 1; +} DzlSuggestionEntryBufferPrivate; + +enum { + PROP_0, + PROP_SUGGESTION, + N_PROPS +}; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlSuggestionEntryBuffer, dzl_suggestion_entry_buffer, GTK_TYPE_ENTRY_BUFFER) + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_suggestion_entry_buffer_drop_suggestion (DzlSuggestionEntryBuffer *self) +{ + DzlSuggestionEntryBufferPrivate *priv = dzl_suggestion_entry_buffer_get_instance_private (self); + + g_assert (DZL_IS_SUGGESTION_ENTRY_BUFFER (self)); + + if (priv->suffix != NULL) + { + guint length = GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->get_length (GTK_ENTRY_BUFFER (self)); + guint suffix_len = strlen (priv->suffix); + + g_clear_pointer (&priv->suffix, g_free); + gtk_entry_buffer_emit_deleted_text (GTK_ENTRY_BUFFER (self), length, suffix_len); + } +} + +static void +dzl_suggestion_entry_buffer_insert_suggestion (DzlSuggestionEntryBuffer *self) +{ + DzlSuggestionEntryBufferPrivate *priv = dzl_suggestion_entry_buffer_get_instance_private (self); + + g_assert (DZL_IS_SUGGESTION_ENTRY_BUFFER (self)); + + if (priv->suggestion != NULL) + { + g_autofree gchar *suffix = NULL; + const gchar *text; + guint length; + + length = GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->get_length (GTK_ENTRY_BUFFER (self)); + text = GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->get_text (GTK_ENTRY_BUFFER (self), NULL); + suffix = dzl_suggestion_suggest_suffix (priv->suggestion, text); + + if (suffix != NULL) + { + priv->suffix = g_steal_pointer (&suffix); + gtk_entry_buffer_emit_inserted_text (GTK_ENTRY_BUFFER (self), + length, + priv->suffix, + g_utf8_strlen (priv->suffix, -1)); + } + } +} + +const gchar * +dzl_suggestion_entry_buffer_get_typed_text (DzlSuggestionEntryBuffer *self) +{ + g_return_val_if_fail (DZL_IS_SUGGESTION_ENTRY_BUFFER (self), NULL); + + return GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->get_text (GTK_ENTRY_BUFFER (self), NULL); +} + +static const gchar * +dzl_suggestion_entry_buffer_get_text (GtkEntryBuffer *buffer, + gsize *n_bytes) +{ + DzlSuggestionEntryBuffer *self = (DzlSuggestionEntryBuffer *)buffer; + DzlSuggestionEntryBufferPrivate *priv = dzl_suggestion_entry_buffer_get_instance_private (self); + + g_assert (DZL_IS_SUGGESTION_ENTRY_BUFFER (self)); + + if (priv->text == NULL) + { + const gchar *text; + GString *str = NULL; + + text = GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->get_text (buffer, n_bytes); + + str = g_string_new (text); + if (priv->suffix != NULL) + g_string_append (str, priv->suffix); + priv->text = g_string_free (str, FALSE); + } + + return priv->text; +} + +static guint +dzl_suggestion_entry_buffer_get_length (GtkEntryBuffer *buffer) +{ + DzlSuggestionEntryBuffer *self = (DzlSuggestionEntryBuffer *)buffer; + DzlSuggestionEntryBufferPrivate *priv = dzl_suggestion_entry_buffer_get_instance_private (self); + guint ret; + + g_assert (GTK_IS_ENTRY_BUFFER (buffer)); + + ret = GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->get_length (buffer); + + if (priv->suffix != NULL) + ret += strlen (priv->suffix); + + return ret; +} + +static void +dzl_suggestion_entry_buffer_inserted_text (GtkEntryBuffer *buffer, + guint position, + const gchar *chars, + guint n_chars) +{ + DzlSuggestionEntryBuffer *self = (DzlSuggestionEntryBuffer *)buffer; + DzlSuggestionEntryBufferPrivate *priv = dzl_suggestion_entry_buffer_get_instance_private (self); + + g_assert (GTK_IS_ENTRY_BUFFER (buffer)); + + g_clear_pointer (&priv->text, g_free); + + GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->inserted_text (buffer, position, chars, n_chars); +} + +static void +dzl_suggestion_entry_buffer_deleted_text (GtkEntryBuffer *buffer, + guint position, + guint n_chars) +{ + DzlSuggestionEntryBuffer *self = (DzlSuggestionEntryBuffer *)buffer; + DzlSuggestionEntryBufferPrivate *priv = dzl_suggestion_entry_buffer_get_instance_private (self); + + g_assert (GTK_IS_ENTRY_BUFFER (buffer)); + + g_clear_pointer (&priv->text, g_free); + + GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->deleted_text (buffer, position, n_chars); +} + +static guint +dzl_suggestion_entry_buffer_insert_text (GtkEntryBuffer *buffer, + guint position, + const gchar *chars, + guint n_chars) +{ + DzlSuggestionEntryBuffer *self = (DzlSuggestionEntryBuffer *)buffer; + DzlSuggestionEntryBufferPrivate *priv = dzl_suggestion_entry_buffer_get_instance_private (self); + guint ret = 0; + + g_assert (GTK_IS_ENTRY_BUFFER (buffer)); + g_assert (chars != NULL || n_chars == 0); + g_assert (priv->in_insert == FALSE); + + priv->in_insert = TRUE; + + if (n_chars == 0) + goto failure; + + dzl_suggestion_entry_buffer_drop_suggestion (self); + + ret = GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->insert_text (buffer, position, chars, n_chars); + if (ret < n_chars) + goto failure; + + dzl_suggestion_entry_buffer_insert_suggestion (self); + +failure: + priv->in_insert = FALSE; + + return ret; +} + +static guint +dzl_suggestion_entry_buffer_delete_text (GtkEntryBuffer *buffer, + guint position, + guint n_chars) +{ + DzlSuggestionEntryBuffer *self = (DzlSuggestionEntryBuffer *)buffer; + DzlSuggestionEntryBufferPrivate *priv = dzl_suggestion_entry_buffer_get_instance_private (self); + guint length; + guint ret = 0; + + g_assert (GTK_IS_ENTRY_BUFFER (buffer)); + + priv->in_delete = TRUE; + + length = GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->get_length (buffer); + + if (position >= length) + goto failure; + + if (position + n_chars > length) + n_chars = length - position; + + dzl_suggestion_entry_buffer_drop_suggestion (self); + + ret = GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->delete_text (buffer, position, n_chars); + + if (ret != 0 && priv->suggestion != NULL) + dzl_suggestion_entry_buffer_insert_suggestion (self); + +failure: + priv->in_delete = FALSE; + + return ret; +} + +static void +dzl_suggestion_entry_buffer_finalize (GObject *object) +{ + DzlSuggestionEntryBuffer *self = (DzlSuggestionEntryBuffer *)object; + DzlSuggestionEntryBufferPrivate *priv = dzl_suggestion_entry_buffer_get_instance_private (self); + + g_clear_object (&priv->suggestion); + g_clear_pointer (&priv->text, g_free); + g_clear_pointer (&priv->suffix, g_free); + + G_OBJECT_CLASS (dzl_suggestion_entry_buffer_parent_class)->finalize (object); +} + +static void +dzl_suggestion_entry_buffer_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSuggestionEntryBuffer *self = DZL_SUGGESTION_ENTRY_BUFFER (object); + + switch (prop_id) + { + case PROP_SUGGESTION: + g_value_set_object (value, dzl_suggestion_entry_buffer_get_suggestion (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_suggestion_entry_buffer_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSuggestionEntryBuffer *self = DZL_SUGGESTION_ENTRY_BUFFER (object); + + switch (prop_id) + { + case PROP_SUGGESTION: + dzl_suggestion_entry_buffer_set_suggestion (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_suggestion_entry_buffer_class_init (DzlSuggestionEntryBufferClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkEntryBufferClass *entry_buffer_class = GTK_ENTRY_BUFFER_CLASS (klass); + + object_class->finalize = dzl_suggestion_entry_buffer_finalize; + object_class->get_property = dzl_suggestion_entry_buffer_get_property; + object_class->set_property = dzl_suggestion_entry_buffer_set_property; + + entry_buffer_class->inserted_text = dzl_suggestion_entry_buffer_inserted_text; + entry_buffer_class->deleted_text = dzl_suggestion_entry_buffer_deleted_text; + entry_buffer_class->get_text = dzl_suggestion_entry_buffer_get_text; + entry_buffer_class->get_length = dzl_suggestion_entry_buffer_get_length; + entry_buffer_class->insert_text = dzl_suggestion_entry_buffer_insert_text; + entry_buffer_class->delete_text = dzl_suggestion_entry_buffer_delete_text; + + properties [PROP_SUGGESTION] = + g_param_spec_object ("suggestion", + "Suggestion", + "The suggestion currently selected", + DZL_TYPE_SUGGESTION, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_suggestion_entry_buffer_init (DzlSuggestionEntryBuffer *self) +{ +} + +DzlSuggestionEntryBuffer * +dzl_suggestion_entry_buffer_new (void) +{ + return g_object_new (DZL_TYPE_SUGGESTION_ENTRY_BUFFER, NULL); +} + +/** + * dzl_suggestion_entry_buffer_get_suggestion: + * @self: a #DzlSuggestionEntryBuffer + * + * Gets the #DzlSuggestion that is the current "preview suffix" of the + * text in the entry. + * + * Returns: (transfer none) (nullable): An #DzlSuggestion or %NULL. + */ +DzlSuggestion * +dzl_suggestion_entry_buffer_get_suggestion (DzlSuggestionEntryBuffer *self) +{ + DzlSuggestionEntryBufferPrivate *priv = dzl_suggestion_entry_buffer_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SUGGESTION_ENTRY_BUFFER (self), NULL); + + return priv->suggestion; +} + +/** + * dzl_suggestion_entry_buffer_set_suggestion: + * @self: a #DzlSuggestionEntryBuffer + * @suggestion: (nullable): An #DzlSuggestion or %NULL + * + * Sets the current suggestion for the entry buffer. + * + * The suggestion is used to get a potential suffix for the current entry + * text. This allows the entry to show "preview text" after the entered + * text for what might be inserted should they activate the current item. + */ +void +dzl_suggestion_entry_buffer_set_suggestion (DzlSuggestionEntryBuffer *self, + DzlSuggestion *suggestion) +{ + DzlSuggestionEntryBufferPrivate *priv = dzl_suggestion_entry_buffer_get_instance_private (self); + + g_return_if_fail (DZL_IS_SUGGESTION_ENTRY_BUFFER (self)); + g_return_if_fail (!suggestion || DZL_IS_SUGGESTION (suggestion)); + + if (priv->suggestion != suggestion) + { + dzl_suggestion_entry_buffer_drop_suggestion (self); + g_set_object (&priv->suggestion, suggestion); + if (!priv->in_delete && !priv->in_insert) + dzl_suggestion_entry_buffer_insert_suggestion (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SUGGESTION]); + } +} + +guint +dzl_suggestion_entry_buffer_get_typed_length (DzlSuggestionEntryBuffer *self) +{ + const gchar *text; + + g_return_val_if_fail (DZL_IS_SUGGESTION_ENTRY_BUFFER (self), 0); + + text = dzl_suggestion_entry_buffer_get_typed_text (self); + + return text ? g_utf8_strlen (text, -1) : 0; +} + +void +dzl_suggestion_entry_buffer_commit (DzlSuggestionEntryBuffer *self) +{ + DzlSuggestionEntryBufferPrivate *priv = dzl_suggestion_entry_buffer_get_instance_private (self); + + g_return_if_fail (DZL_IS_SUGGESTION_ENTRY_BUFFER (self)); + + if (priv->suffix != NULL) + { + g_autofree gchar *suffix = g_steal_pointer (&priv->suffix); + guint position; + + g_clear_object (&priv->suggestion); + position = GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->get_length (GTK_ENTRY_BUFFER (self)); + GTK_ENTRY_BUFFER_CLASS (dzl_suggestion_entry_buffer_parent_class)->insert_text (GTK_ENTRY_BUFFER (self), + position, + suffix, + g_utf8_strlen (suffix, -1)); + } +} diff --git a/src/suggestions/dzl-suggestion-entry-buffer.h b/src/suggestions/dzl-suggestion-entry-buffer.h new file mode 100644 index 0000000..385804c --- /dev/null +++ b/src/suggestions/dzl-suggestion-entry-buffer.h @@ -0,0 +1,61 @@ +/* dzl-suggestion-entry-buffer.h + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#ifndef DZL_SUGGESTION_ENTRY_BUFFER_H +#define DZL_SUGGESTION_ENTRY_BUFFER_H + +#include + +#include "dzl-version-macros.h" + +#include "dzl-suggestion.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SUGGESTION_ENTRY_BUFFER (dzl_suggestion_entry_buffer_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlSuggestionEntryBuffer, dzl_suggestion_entry_buffer, DZL, SUGGESTION_ENTRY_BUFFER, GtkEntryBuffer) + +struct _DzlSuggestionEntryBufferClass +{ + GtkEntryBufferClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; +}; + +DZL_AVAILABLE_IN_ALL +DzlSuggestionEntryBuffer *dzl_suggestion_entry_buffer_new (void); +DZL_AVAILABLE_IN_ALL +DzlSuggestion *dzl_suggestion_entry_buffer_get_suggestion (DzlSuggestionEntryBuffer *self); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_entry_buffer_set_suggestion (DzlSuggestionEntryBuffer *self, + DzlSuggestion *suggestion); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_suggestion_entry_buffer_get_typed_text (DzlSuggestionEntryBuffer *self); +DZL_AVAILABLE_IN_ALL +guint dzl_suggestion_entry_buffer_get_typed_length (DzlSuggestionEntryBuffer *self); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_entry_buffer_commit (DzlSuggestionEntryBuffer *self); + +G_END_DECLS + +#endif /* DZL_SUGGESTION_ENTRY_BUFFER_H */ diff --git a/src/suggestions/dzl-suggestion-entry.c b/src/suggestions/dzl-suggestion-entry.c new file mode 100644 index 0000000..7cbb298 --- /dev/null +++ b/src/suggestions/dzl-suggestion-entry.c @@ -0,0 +1,832 @@ +/* dzl-suggestion-entry.c + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-suggestion-entry" + +#include "config.h" + +#include + +#include "suggestions/dzl-suggestion.h" +#include "suggestions/dzl-suggestion-entry.h" +#include "suggestions/dzl-suggestion-entry-buffer.h" +#include "suggestions/dzl-suggestion-popover.h" +#include "suggestions/dzl-suggestion-private.h" +#include "util/dzl-util-private.h" + +#if 0 +# define _TRACE_LEVEL (1<popover); + dzl_suggestion_popover_popup (priv->popover); + + DZL_EXIT; +} + +static void +dzl_suggestion_entry_hide_suggestions (DzlSuggestionEntry *self) +{ + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + DZL_ENTRY; + + g_assert (DZL_IS_SUGGESTION_ENTRY (self)); + + dzl_suggestion_popover_popdown (priv->popover); + + DZL_EXIT; +} + +static gboolean +dzl_suggestion_entry_focus_out_event (GtkWidget *widget, + GdkEventFocus *event) +{ + DzlSuggestionEntry *self = (DzlSuggestionEntry *)widget; + + g_assert (DZL_IS_SUGGESTION_ENTRY (self)); + g_assert (event != NULL); + + g_signal_emit (self, signals [HIDE_SUGGESTIONS], 0); + + return GTK_WIDGET_CLASS (dzl_suggestion_entry_parent_class)->focus_out_event (widget, event); +} + +static void +dzl_suggestion_entry_hierarchy_changed (GtkWidget *widget, + GtkWidget *old_toplevel) +{ + DzlSuggestionEntry *self = (DzlSuggestionEntry *)widget; + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + g_assert (DZL_IS_SUGGESTION_ENTRY (self)); + g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel)); + + if (priv->popover != NULL) + { + GtkWidget *toplevel = gtk_widget_get_ancestor (widget, GTK_TYPE_WINDOW); + + gtk_window_set_transient_for (GTK_WINDOW (priv->popover), GTK_WINDOW (toplevel)); + } +} + +static gboolean +dzl_suggestion_entry_key_press_event (GtkWidget *widget, + GdkEventKey *key) +{ + DzlSuggestionEntry *self = (DzlSuggestionEntry *)widget; + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + g_assert (DZL_IS_SUGGESTION_ENTRY (self)); + + /* + * If Tab was pressed, and there is uncommitted suggested text, + * commit it and stop propagation of the key press. + */ + if (key->keyval == GDK_KEY_Tab && (key->state & GDK_MODIFIER_MASK) == 0) + { + const gchar *typed_text; + DzlSuggestion *suggestion; + + typed_text = dzl_suggestion_entry_buffer_get_typed_text (priv->buffer); + suggestion = dzl_suggestion_popover_get_selected (priv->popover); + + if (typed_text != NULL && suggestion != NULL) + { + g_autofree gchar *replace = dzl_suggestion_replace_typed_text (suggestion, typed_text); + + g_signal_handler_block (self, priv->changed_handler); + + if (replace != NULL) + gtk_entry_set_text (GTK_ENTRY (self), replace); + else + dzl_suggestion_entry_buffer_commit (priv->buffer); + gtk_editable_set_position (GTK_EDITABLE (self), -1); + + g_signal_handler_unblock (self, priv->changed_handler); + + return GDK_EVENT_STOP; + } + } + + return GTK_WIDGET_CLASS (dzl_suggestion_entry_parent_class)->key_press_event (widget, key); +} + +static void +dzl_suggestion_entry_update_attrs (DzlSuggestionEntry *self) +{ + PangoAttribute *attr; + PangoAttrList *list; + const gchar *typed_text; + const gchar *text; + GdkRGBA rgba; + + DZL_ENTRY; + + g_assert (DZL_IS_SUGGESTION_ENTRY (self)); + + gdk_rgba_parse (&rgba, "#666666"); + + text = gtk_entry_get_text (GTK_ENTRY (self)); + typed_text = dzl_suggestion_entry_get_typed_text (self); + + list = pango_attr_list_new (); + attr = pango_attr_foreground_new (rgba.red * 0xFFFF, rgba.green * 0xFFFF, rgba.blue * 0xFFFF); + attr->start_index = strlen (typed_text); + attr->end_index = strlen (text); + pango_attr_list_insert (list, attr); + gtk_entry_set_attributes (GTK_ENTRY (self), list); + pango_attr_list_unref (list); + + DZL_EXIT; +} + +static void +dzl_suggestion_entry_changed (GtkEditable *editable) +{ + DzlSuggestionEntry *self = (DzlSuggestionEntry *)editable; + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + DzlSuggestion *suggestion; + const gchar *text; + + DZL_ENTRY; + + g_assert (DZL_IS_SUGGESTION_ENTRY (self)); + + /* + * If we aren't focused, just ignore everything. One such example might be + * updating an URI in a webbrowser. + */ + if (!gtk_widget_has_focus (GTK_WIDGET (editable))) + return; + + g_signal_handler_block (self, priv->changed_handler); + + text = dzl_suggestion_entry_buffer_get_typed_text (priv->buffer); + + if (text == NULL || *text == '\0') + { + dzl_suggestion_entry_buffer_set_suggestion (priv->buffer, NULL); + g_signal_emit (self, signals [HIDE_SUGGESTIONS], 0); + DZL_GOTO (finish); + } + + g_signal_emit (self, signals [SHOW_SUGGESTIONS], 0); + + suggestion = dzl_suggestion_popover_get_selected (priv->popover); + + if (suggestion != NULL) + { + g_object_ref (suggestion); + dzl_suggestion_entry_buffer_set_suggestion (priv->buffer, suggestion); + g_object_unref (suggestion); + } + +finish: + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TYPED_TEXT]); + + g_signal_handler_unblock (self, priv->changed_handler); + + dzl_suggestion_entry_update_attrs (self); + + DZL_EXIT; +} + +static void +dzl_suggestion_entry_suggestion_activated (DzlSuggestionEntry *self, + DzlSuggestion *suggestion, + DzlSuggestionPopover *popover) +{ + DZL_ENTRY; + + g_assert (DZL_IS_SUGGESTION_ENTRY (self)); + g_assert (DZL_IS_SUGGESTION (suggestion)); + g_assert (DZL_IS_SUGGESTION_POPOVER (popover)); + + g_signal_emit (self, signals [SUGGESTION_ACTIVATED], 0, suggestion); + g_signal_emit (self, signals [HIDE_SUGGESTIONS], 0); + gtk_entry_set_text (GTK_ENTRY (self), ""); + + DZL_EXIT; +} + +static void +dzl_suggestion_entry_move_suggestion (DzlSuggestionEntry *self, + gint amount) +{ + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + g_assert (DZL_IS_SUGGESTION_ENTRY (self)); + + dzl_suggestion_popover_move_by (priv->popover, amount); +} + +static void +dzl_suggestion_entry_activate_suggestion (DzlSuggestionEntry *self) +{ + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + DZL_ENTRY; + + g_assert (DZL_IS_SUGGESTION_ENTRY (self)); + + dzl_suggestion_popover_activate_selected (priv->popover); + + DZL_EXIT; +} + +static void +dzl_suggestion_entry_constructed (GObject *object) +{ + DzlSuggestionEntry *self = (DzlSuggestionEntry *)object; + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + G_OBJECT_CLASS (dzl_suggestion_entry_parent_class)->constructed (object); + + gtk_entry_set_buffer (GTK_ENTRY (self), GTK_ENTRY_BUFFER (priv->buffer)); +} + +static void +dzl_suggestion_entry_destroy (GtkWidget *widget) +{ + DzlSuggestionEntry *self = (DzlSuggestionEntry *)widget; + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + if (priv->func_data_destroy != NULL) + { + GDestroyNotify notify = g_steal_pointer (&priv->func_data_destroy); + gpointer notify_data = g_steal_pointer (&priv->func_data); + + notify (notify_data); + } + + if (priv->popover != NULL) + gtk_widget_destroy (GTK_WIDGET (priv->popover)); + + g_clear_object (&priv->model); + + g_assert (priv->popover == NULL); + + GTK_WIDGET_CLASS (dzl_suggestion_entry_parent_class)->destroy (widget); +} + +static void +dzl_suggestion_entry_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSuggestionEntry *self = DZL_SUGGESTION_ENTRY (object); + + switch (prop_id) + { + case PROP_MODEL: + g_value_set_object (value, dzl_suggestion_entry_get_model (self)); + break; + + case PROP_TYPED_TEXT: + g_value_set_string (value, dzl_suggestion_entry_get_typed_text (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_suggestion_entry_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSuggestionEntry *self = DZL_SUGGESTION_ENTRY (object); + + switch (prop_id) + { + case PROP_MODEL: + dzl_suggestion_entry_set_model (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_suggestion_entry_class_init (DzlSuggestionEntryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkBindingSet *bindings; + + object_class->constructed = dzl_suggestion_entry_constructed; + object_class->get_property = dzl_suggestion_entry_get_property; + object_class->set_property = dzl_suggestion_entry_set_property; + + widget_class->destroy = dzl_suggestion_entry_destroy; + widget_class->focus_out_event = dzl_suggestion_entry_focus_out_event; + widget_class->hierarchy_changed = dzl_suggestion_entry_hierarchy_changed; + widget_class->key_press_event = dzl_suggestion_entry_key_press_event; + + klass->hide_suggestions = dzl_suggestion_entry_hide_suggestions; + klass->show_suggestions = dzl_suggestion_entry_show_suggestions; + klass->move_suggestion = dzl_suggestion_entry_move_suggestion; + + properties [PROP_MODEL] = + g_param_spec_object ("model", + "Model", + "The model to be visualized", + G_TYPE_LIST_MODEL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TYPED_TEXT] = + g_param_spec_string ("typed-text", + "Typed Text", + "Typed text into the entry, does not include suggested text", + NULL, + (G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + signals [HIDE_SUGGESTIONS] = + g_signal_new ("hide-suggestions", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (DzlSuggestionEntryClass, hide_suggestions), + NULL, NULL, NULL, G_TYPE_NONE, 0); + + /** + * DzlSuggestionEntry::move-suggestion: + * @self: A #DzlSuggestionEntry + * @amount: The number of items to move + * + * This moves the selected suggestion in the popover by the value + * provided. -1 moves up one row, 1, moves down a row. + */ + signals [MOVE_SUGGESTION] = + g_signal_new ("move-suggestion", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (DzlSuggestionEntryClass, move_suggestion), + NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_INT); + + signals [SHOW_SUGGESTIONS] = + g_signal_new ("show-suggestions", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (DzlSuggestionEntryClass, show_suggestions), + NULL, NULL, NULL, G_TYPE_NONE, 0); + + signals [SUGGESTION_ACTIVATED] = + g_signal_new ("suggestion-activated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlSuggestionEntryClass, suggestion_activated), + NULL, NULL, NULL, G_TYPE_NONE, 1, DZL_TYPE_SUGGESTION); + + widget_class->activate_signal = signals [ACTIVATE_SUGGESTION] = + g_signal_new_class_handler ("activate-suggestion", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_CALLBACK (dzl_suggestion_entry_activate_suggestion), + NULL, NULL, NULL, G_TYPE_NONE, 0); + + bindings = gtk_binding_set_by_class (klass); + gtk_binding_entry_add_signal (bindings, GDK_KEY_Escape, 0, "hide-suggestions", 0); + gtk_binding_entry_add_signal (bindings, GDK_KEY_space, GDK_CONTROL_MASK, "show-suggestions", 0); + gtk_binding_entry_add_signal (bindings, GDK_KEY_Up, 0, "move-suggestion", 1, G_TYPE_INT, -1); + gtk_binding_entry_add_signal (bindings, GDK_KEY_Down, 0, "move-suggestion", 1, G_TYPE_INT, 1); + gtk_binding_entry_add_signal (bindings, GDK_KEY_Page_Up, 0, "move-suggestion", 1, G_TYPE_INT, -10); + gtk_binding_entry_add_signal (bindings, GDK_KEY_KP_Page_Up, 0, "move-suggestion", 1, G_TYPE_INT, -10); + gtk_binding_entry_add_signal (bindings, GDK_KEY_Prior, 0, "move-suggestion", 1, G_TYPE_INT, -10); + gtk_binding_entry_add_signal (bindings, GDK_KEY_Next, 0, "move-suggestion", 1, G_TYPE_INT, 10); + gtk_binding_entry_add_signal (bindings, GDK_KEY_Page_Down, 0, "move-suggestion", 1, G_TYPE_INT, 10); + gtk_binding_entry_add_signal (bindings, GDK_KEY_KP_Page_Down, 0, "move-suggestion", 1, G_TYPE_INT, 10); + gtk_binding_entry_add_signal (bindings, GDK_KEY_Return, 0, "activate-suggestion", 0); + + changed_signal_id = g_signal_lookup ("changed", GTK_TYPE_ENTRY); +} + +static void +dzl_suggestion_entry_init (DzlSuggestionEntry *self) +{ + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + priv->func = dzl_suggestion_entry_default_position_func; + + priv->changed_handler = + g_signal_connect_after (self, + "changed", + G_CALLBACK (dzl_suggestion_entry_changed), + NULL); + + priv->popover = g_object_new (DZL_TYPE_SUGGESTION_POPOVER, + "destroy-with-parent", TRUE, + "modal", FALSE, + "relative-to", self, + "type", GTK_WINDOW_POPUP, + NULL); + g_signal_connect_object (priv->popover, + "suggestion-activated", + G_CALLBACK (dzl_suggestion_entry_suggestion_activated), + self, + G_CONNECT_SWAPPED); + g_signal_connect (priv->popover, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &priv->popover); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self)), + "suggestion"); + + priv->buffer = dzl_suggestion_entry_buffer_new (); +} + +GtkWidget * +dzl_suggestion_entry_new (void) +{ + return g_object_new (DZL_TYPE_SUGGESTION_ENTRY, NULL); +} + +static GObject * +dzl_suggestion_entry_get_internal_child (GtkBuildable *buildable, + GtkBuilder *builder, + const gchar *childname) +{ + DzlSuggestionEntry *self = (DzlSuggestionEntry *)buildable; + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + if (g_strcmp0 (childname, "popover") == 0) + return G_OBJECT (priv->popover); + + return NULL; +} + +static void +buildable_iface_init (GtkBuildableIface *iface) +{ + iface->get_internal_child = dzl_suggestion_entry_get_internal_child; +} + +static void +dzl_suggestion_entry_set_selection_bounds (GtkEditable *editable, + gint start_pos, + gint end_pos) +{ + DzlSuggestionEntry *self = (DzlSuggestionEntry *)editable; + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + g_assert (DZL_IS_SUGGESTION_ENTRY (self)); + + g_signal_handler_block (self, priv->changed_handler); + + if (end_pos < 0) + end_pos = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (priv->buffer)); + + if (end_pos > (gint)dzl_suggestion_entry_buffer_get_typed_length (priv->buffer)) + dzl_suggestion_entry_buffer_commit (priv->buffer); + + editable_parent_iface->set_selection_bounds (editable, start_pos, end_pos); + + g_signal_handler_unblock (self, priv->changed_handler); +} + +static void +editable_iface_init (GtkEditableInterface *iface) +{ + editable_parent_iface = g_type_interface_peek_parent (iface); + + iface->set_selection_bounds = dzl_suggestion_entry_set_selection_bounds; +} + + +/** + * dzl_suggestion_entry_get_model: + * @self: a #DzlSuggestionEntry + * + * Gets the model being visualized. + * + * Returns: (nullable) (transfer none): A #GListModel or %NULL. + */ +GListModel * +dzl_suggestion_entry_get_model (DzlSuggestionEntry *self) +{ + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SUGGESTION_ENTRY (self), NULL); + + return priv->model; +} + +void +dzl_suggestion_entry_set_model (DzlSuggestionEntry *self, + GListModel *model) +{ + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + DZL_ENTRY; + + g_return_if_fail (DZL_IS_SUGGESTION_ENTRY (self)); + g_return_if_fail (!model || g_type_is_a (g_list_model_get_item_type (model), DZL_TYPE_SUGGESTION)); + + if (g_set_object (&priv->model, model)) + { + DZL_TRACE_MSG ("Model has %u items", + model ? g_list_model_get_n_items (model) : 0); + dzl_suggestion_popover_set_model (priv->popover, model); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]); + dzl_suggestion_entry_update_attrs (self); + } + + DZL_EXIT; +} + +/** + * dzl_suggestion_entry_get_suggestion: + * @self: a #DzlSuggestionEntry + * + * Gets the currently selected suggestion. + * + * Returns: (nullable) (transfer none): An #DzlSuggestion or %NULL. + */ +DzlSuggestion * +dzl_suggestion_entry_get_suggestion (DzlSuggestionEntry *self) +{ + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SUGGESTION_ENTRY (self), NULL); + + return dzl_suggestion_popover_get_selected (priv->popover); +} + +void +dzl_suggestion_entry_set_suggestion (DzlSuggestionEntry *self, + DzlSuggestion *suggestion) +{ + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + DZL_ENTRY; + + g_return_if_fail (DZL_IS_SUGGESTION_ENTRY (self)); + g_return_if_fail (!suggestion || DZL_IS_SUGGESTION_ENTRY (suggestion)); + + dzl_suggestion_popover_set_selected (priv->popover, suggestion); + dzl_suggestion_entry_buffer_set_suggestion (priv->buffer, suggestion); + + DZL_EXIT; +} + +const gchar * +dzl_suggestion_entry_get_typed_text (DzlSuggestionEntry *self) +{ + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SUGGESTION_ENTRY (self), NULL); + + return dzl_suggestion_entry_buffer_get_typed_text (priv->buffer); +} + +void +dzl_suggestion_entry_default_position_func (DzlSuggestionEntry *self, + GdkRectangle *area, + gboolean *is_absolute, + gpointer user_data) +{ + GtkAllocation alloc; + + g_return_if_fail (DZL_IS_SUGGESTION_ENTRY (self)); + g_return_if_fail (area != NULL); + g_return_if_fail (is_absolute != NULL); + + *is_absolute = FALSE; + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + area->y += alloc.height; + area->height = 300; +} + +/** + * dzl_suggestion_entry_window_position_func: + * + * This is a #DzlSuggestionPositionFunc that can be used to make the suggestion + * popover the full width of the window. It is similar to what you might find + * in a web browser. + */ +void +dzl_suggestion_entry_window_position_func (DzlSuggestionEntry *self, + GdkRectangle *area, + gboolean *is_absolute, + gpointer user_data) +{ + GtkWidget *toplevel; + + g_return_if_fail (DZL_IS_SUGGESTION_ENTRY (self)); + g_return_if_fail (area != NULL); + g_return_if_fail (is_absolute != NULL); + + toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW); + + if (toplevel != NULL) + { + GtkWidget *child = gtk_bin_get_child (GTK_BIN (toplevel)); + GtkAllocation alloc; + gint x, y; + gint height = 300; + + gtk_widget_translate_coordinates (child, toplevel, 0, 0, &x, &y); + gtk_widget_get_allocation (child, &alloc); + gtk_window_get_size (GTK_WINDOW (toplevel), NULL, &height); + + area->x = x; + area->y = y; + area->width = alloc.width; + area->height = MAX (300, height / 2); + + /* If our widget would get obscurred, adjust it */ + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + gtk_widget_translate_coordinates (GTK_WIDGET (self), toplevel, + 0, alloc.height, NULL, &y); + if (y > area->y) + area->y = y; + + *is_absolute = TRUE; + + return; + } + + dzl_suggestion_entry_default_position_func (self, area, is_absolute, NULL); +} + +/** + * dzl_suggestion_entry_set_position_func: + * @self: a #DzlSuggestionEntry + * @func: (scope async) (closure func_data) (destroy func_data_destroy) (nullable): + * A function to call to position the popover, or %NULL to set the default. + * @func_data: (nullable): closure data for @func + * @func_data_destroy: (nullable): a destroy notify for @func_data + * + * Sets a position func to position the popover. + * + * In @func, you should set the height of the rectangle to the maximum height + * that the popover should be allowed to grow. + * + * Since: 3.26 + */ +void +dzl_suggestion_entry_set_position_func (DzlSuggestionEntry *self, + DzlSuggestionPositionFunc func, + gpointer func_data, + GDestroyNotify func_data_destroy) +{ + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + GDestroyNotify notify = NULL; + gpointer notify_data = NULL; + + g_return_if_fail (DZL_IS_SUGGESTION_ENTRY (self)); + + if (func == NULL) + { + func = dzl_suggestion_entry_default_position_func; + func_data = NULL; + func_data_destroy = NULL; + } + + if (priv->func_data_destroy != NULL) + { + notify = priv->func_data_destroy; + notify_data = priv->func_data; + } + + priv->func = func; + priv->func_data = func_data; + priv->func_data_destroy = func_data_destroy; + + if (notify) + notify (notify_data); +} + +void +_dzl_suggestion_entry_reposition (DzlSuggestionEntry *self, + DzlSuggestionPopover *popover) +{ + DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self); + GtkWidget *toplevel; + GdkWindow *window; + GtkAllocation alloc; + gboolean is_absolute = FALSE; + gint x; + gint y; + + g_return_if_fail (DZL_IS_SUGGESTION_ENTRY (self)); + g_return_if_fail (DZL_IS_SUGGESTION_POPOVER (popover)); + + if (!gtk_widget_get_realized (GTK_WIDGET (self)) || + !gtk_widget_get_realized (GTK_WIDGET (popover))) + return; + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self)); + window = gtk_widget_get_window (toplevel); + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + alloc.x = 0; + alloc.y = 0; + + priv->func (self, &alloc, &is_absolute, priv->func_data); + + _dzl_suggestion_popover_set_max_height (priv->popover, alloc.height); + + if (!is_absolute) + { + gtk_widget_translate_coordinates (GTK_WIDGET (self), toplevel, 0, 0, &x, &y); + alloc.x += x; + alloc.y += y; + } + + gdk_window_get_position (window, &x, &y); + alloc.x += x; + alloc.y += y; + + _dzl_suggestion_popover_adjust_margin (popover, &alloc); + + gtk_widget_set_size_request (GTK_WIDGET (popover), alloc.width, -1); + gtk_window_move (GTK_WINDOW (popover), alloc.x, alloc.y); +} diff --git a/src/suggestions/dzl-suggestion-entry.h b/src/suggestions/dzl-suggestion-entry.h new file mode 100644 index 0000000..b11e40e --- /dev/null +++ b/src/suggestions/dzl-suggestion-entry.h @@ -0,0 +1,108 @@ +/* dzl-suggestion-entry.h + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#ifndef DZL_SUGGESTION_ENTRY_H +#define DZL_SUGGESTION_ENTRY_H + +#include + +#include "dzl-version-macros.h" + +#include "dzl-suggestion.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SUGGESTION_ENTRY (dzl_suggestion_entry_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlSuggestionEntry, dzl_suggestion_entry, DZL, SUGGESTION_ENTRY, GtkEntry) + +/** + * DzlSuggestionPositionFunc: + * @entry: a #DzlSuggestionEntry + * @area: (inout): location to place the popover + * @is_absolute: (inout): If the area is in absolute coordinates + * @user_data: closure data + * + * Positions the popover in the coordinates defined by @area. + * + * If @is_absolute is set to %TRUE, then absolute coordinates are used. + * Otherwise, the position is expected to be relative to @entry. + * + * Since: 3.26 + */ +typedef void (*DzlSuggestionPositionFunc) (DzlSuggestionEntry *entry, + GdkRectangle *area, + gboolean *is_absolute, + gpointer user_data); + +struct _DzlSuggestionEntryClass +{ + GtkEntryClass parent_class; + + void (*hide_suggestions) (DzlSuggestionEntry *self); + void (*show_suggestions) (DzlSuggestionEntry *self); + void (*move_suggestion ) (DzlSuggestionEntry *self, + gint amount); + void (*suggestion_activated) (DzlSuggestionEntry *self, + DzlSuggestion *suggestion); + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_suggestion_entry_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_entry_set_model (DzlSuggestionEntry *self, + GListModel *model); +DZL_AVAILABLE_IN_ALL +GListModel *dzl_suggestion_entry_get_model (DzlSuggestionEntry *self); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_suggestion_entry_get_typed_text (DzlSuggestionEntry *self); +DZL_AVAILABLE_IN_ALL +DzlSuggestion *dzl_suggestion_entry_get_suggestion (DzlSuggestionEntry *self); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_entry_set_suggestion (DzlSuggestionEntry *self, + DzlSuggestion *suggestion); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_entry_set_position_func (DzlSuggestionEntry *self, + DzlSuggestionPositionFunc func, + gpointer func_data, + GDestroyNotify func_data_destroy); + +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_entry_default_position_func (DzlSuggestionEntry *self, + GdkRectangle *area, + gboolean *is_absolute, + gpointer user_data); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_entry_window_position_func (DzlSuggestionEntry *self, + GdkRectangle *area, + gboolean *is_absolute, + gpointer user_data); + +G_END_DECLS + +#endif /* DZL_SUGGESTION_ENTRY_H */ diff --git a/src/suggestions/dzl-suggestion-popover.c b/src/suggestions/dzl-suggestion-popover.c new file mode 100644 index 0000000..d0a6672 --- /dev/null +++ b/src/suggestions/dzl-suggestion-popover.c @@ -0,0 +1,973 @@ +/* dzl-suggestion-popover.c + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-suggestion-popover" + +#include "config.h" + +#include + +#include "animation/dzl-animation.h" +#include "suggestions/dzl-suggestion.h" +#include "suggestions/dzl-suggestion-entry.h" +#include "suggestions/dzl-suggestion-popover.h" +#include "suggestions/dzl-suggestion-private.h" +#include "suggestions/dzl-suggestion-row.h" +#include "util/dzl-util-private.h" +#include "widgets/dzl-elastic-bin.h" +#include "widgets/dzl-list-box.h" + +struct _DzlSuggestionPopover +{ + GtkWindow parent_instance; + + GtkWidget *relative_to; + GtkWindow *transient_for; + GtkRevealer *revealer; + GtkScrolledWindow *scrolled_window; + DzlListBox *list_box; + GtkBox *top_box; + + DzlAnimation *scroll_anim; + + GListModel *model; + + GType row_type; + + gulong delete_event_handler; + gulong configure_event_handler; + gulong size_allocate_handler; + gulong items_changed_handler; +}; + +enum { + PROP_0, + PROP_MODEL, + PROP_RELATIVE_TO, + PROP_SELECTED, + N_PROPS +}; + +enum { + SUGGESTION_ACTIVATED, + N_SIGNALS +}; + +G_DEFINE_TYPE (DzlSuggestionPopover, dzl_suggestion_popover, GTK_TYPE_WINDOW) + +static GParamSpec *properties [N_PROPS]; +static guint signals [N_SIGNALS]; + +static gdouble +check_range (gdouble lower, + gdouble page_size, + gdouble y1, + gdouble y2) +{ + gdouble upper = lower + page_size; + + /* + * The goal here is to determine if y1 and y2 fall within lower and upper. We + * will determine the new value to set to ensure the minimum amount to scroll + * to show the rows. + */ + + /* Do nothing if we are all visible */ + if (y1 >= lower && y2 <= upper) + return lower; + + /* We might need to scroll y2 into view */ + if (y2 > upper) + return y2 - page_size; + + return y1; +} + +static void +dzl_suggestion_popover_select_row (DzlSuggestionPopover *self, + GtkListBoxRow *row) +{ + GtkAdjustment *adj; + GtkAllocation alloc; + gdouble y1; + gdouble y2; + gdouble page_size; + gdouble value; + gdouble new_value; + + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + g_assert (GTK_IS_LIST_BOX_ROW (row)); + + gtk_list_box_select_row (GTK_LIST_BOX (self->list_box), row); + + gtk_widget_get_allocation (GTK_WIDGET (row), &alloc); + + /* If there is no allocation yet, ignore things */ + if (alloc.y < 0) + return; + + /* + * We want to ensure that Y1 and Y2 are both within the + * page of the scrolled window. If not, we need to animate + * our scroll position to include that row. + */ + + y1 = alloc.y; + y2 = alloc.y + alloc.height; + + adj = gtk_scrolled_window_get_vadjustment (self->scrolled_window); + page_size = gtk_adjustment_get_page_size (adj); + value = gtk_adjustment_get_value (adj); + + new_value = check_range (value, page_size, y1, y2); + + if (new_value != value) + { + DzlAnimation *anim; + guint duration = 167; + + if (self->scroll_anim != NULL) + { + dzl_animation_stop (self->scroll_anim); + + /* Speed up the animation because we are scrolling while already + * scrolling. We don't want to fall behind. + */ + duration = 84; + } + + anim = dzl_object_animate (adj, + DZL_ANIMATION_EASE_IN_OUT_CUBIC, + duration, + gtk_widget_get_frame_clock (GTK_WIDGET (self->scrolled_window)), + "value", new_value, + NULL); + dzl_set_weak_pointer (&self->scroll_anim, anim); + } +} + +static void +dzl_suggestion_popover_reposition (DzlSuggestionPopover *self) +{ + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + + if (DZL_IS_SUGGESTION_ENTRY (self->relative_to)) + _dzl_suggestion_entry_reposition (DZL_SUGGESTION_ENTRY (self->relative_to), self); +} + +/** + * dzl_suggestion_popover_get_relative_to: + * @self: a #DzlSuggestionPopover + * + * Returns: (transfer none) (nullable): A #GtkWidget or %NULL. + */ +GtkWidget * +dzl_suggestion_popover_get_relative_to (DzlSuggestionPopover *self) +{ + + g_return_val_if_fail (DZL_IS_SUGGESTION_POPOVER (self), NULL); + + return self->relative_to; +} + +void +dzl_suggestion_popover_set_relative_to (DzlSuggestionPopover *self, + GtkWidget *relative_to) +{ + g_return_if_fail (DZL_IS_SUGGESTION_POPOVER (self)); + g_return_if_fail (!relative_to || GTK_IS_WIDGET (relative_to)); + + if (self->relative_to != relative_to) + { + if (self->relative_to != NULL) + { + g_signal_handlers_disconnect_by_func (self->relative_to, + G_CALLBACK (gtk_widget_destroyed), + &self->relative_to); + self->relative_to = NULL; + } + + if (relative_to != NULL) + { + self->relative_to = relative_to; + g_signal_connect (self->relative_to, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &self->relative_to); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RELATIVE_TO]); + } +} + +static void +dzl_suggestion_popover_hide (GtkWidget *widget) +{ + DzlSuggestionPopover *self = (DzlSuggestionPopover *)widget; + + g_return_if_fail (DZL_IS_SUGGESTION_POPOVER (self)); + + if (self->transient_for != NULL) + { + GtkWindowGroup *group = gtk_window_get_group (self->transient_for); + + if (group != NULL) + gtk_window_group_remove_window (group, GTK_WINDOW (self)); + } + + g_signal_handler_disconnect (self->transient_for, self->delete_event_handler); + g_signal_handler_disconnect (self->transient_for, self->size_allocate_handler); + g_signal_handler_disconnect (self->transient_for, self->configure_event_handler); + + self->delete_event_handler = 0; + self->size_allocate_handler = 0; + self->configure_event_handler = 0; + + self->transient_for = NULL; + + GTK_WIDGET_CLASS (dzl_suggestion_popover_parent_class)->hide (widget); +} + +static void +dzl_suggestion_popover_transient_for_size_allocate (DzlSuggestionPopover *self, + GtkAllocation *allocation, + GtkWindow *toplevel) +{ + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + g_assert (allocation != NULL); + g_assert (GTK_IS_WINDOW (toplevel)); + + dzl_suggestion_popover_reposition (self); +} + +static gboolean +dzl_suggestion_popover_transient_for_delete_event (DzlSuggestionPopover *self, + GdkEvent *event, + GtkWindow *toplevel) +{ + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + g_assert (event != NULL); + g_assert (GTK_IS_WINDOW (toplevel)); + + gtk_widget_hide (GTK_WIDGET (self)); + + return FALSE; +} + +static gboolean +dzl_suggestion_popover_transient_for_configure_event (DzlSuggestionPopover *self, + GdkEvent *event, + GtkWindow *toplevel) +{ + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + g_assert (event != NULL); + g_assert (GTK_IS_WINDOW (toplevel)); + + gtk_widget_hide (GTK_WIDGET (self)); + + return FALSE; +} + +static void +dzl_suggestion_popover_show (GtkWidget *widget) +{ + DzlSuggestionPopover *self = (DzlSuggestionPopover *)widget; + + g_return_if_fail (DZL_IS_SUGGESTION_POPOVER (self)); + + if (self->relative_to != NULL) + { + GtkWidget *toplevel; + + toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self->relative_to), GTK_TYPE_WINDOW); + + if (GTK_IS_WINDOW (toplevel)) + { + self->transient_for = GTK_WINDOW (toplevel); + gtk_window_group_add_window (gtk_window_get_group (self->transient_for), + GTK_WINDOW (self)); + self->delete_event_handler = + g_signal_connect_object (toplevel, + "delete-event", + G_CALLBACK (dzl_suggestion_popover_transient_for_delete_event), + self, + G_CONNECT_SWAPPED); + self->size_allocate_handler = + g_signal_connect_object (toplevel, + "size-allocate", + G_CALLBACK (dzl_suggestion_popover_transient_for_size_allocate), + self, + G_CONNECT_SWAPPED | G_CONNECT_AFTER); + self->configure_event_handler = + g_signal_connect_object (toplevel, + "configure-event", + G_CALLBACK (dzl_suggestion_popover_transient_for_configure_event), + self, + G_CONNECT_SWAPPED); + dzl_suggestion_popover_reposition (self); + } + } + + GTK_WIDGET_CLASS (dzl_suggestion_popover_parent_class)->show (widget); +} + +static void +dzl_suggestion_popover_screen_changed (GtkWidget *widget, + GdkScreen *previous_screen) +{ + GdkScreen *screen; + GdkVisual *visual; + + GTK_WIDGET_CLASS (dzl_suggestion_popover_parent_class)->screen_changed (widget, previous_screen); + + screen = gtk_widget_get_screen (widget); + visual = gdk_screen_get_rgba_visual (screen); + + if (visual != NULL) + gtk_widget_set_visual (widget, visual); +} + +static void +dzl_suggestion_popover_realize (GtkWidget *widget) +{ + DzlSuggestionPopover *self = (DzlSuggestionPopover *)widget; + GdkScreen *screen; + GdkVisual *visual; + + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + + screen = gtk_widget_get_screen (widget); + visual = gdk_screen_get_rgba_visual (screen); + + if (visual != NULL) + gtk_widget_set_visual (widget, visual); + + GTK_WIDGET_CLASS (dzl_suggestion_popover_parent_class)->realize (widget); + + dzl_suggestion_popover_reposition (self); +} + +static void +dzl_suggestion_popover_notify_child_revealed (DzlSuggestionPopover *self, + GParamSpec *pspec, + GtkRevealer *revealer) +{ + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + g_assert (GTK_IS_REVEALER (revealer)); + + if (!gtk_revealer_get_reveal_child (self->revealer)) + gtk_widget_hide (GTK_WIDGET (self)); +} + +static void +dzl_suggestion_popover_list_box_row_activated (DzlSuggestionPopover *self, + DzlSuggestionRow *row, + GtkListBox *list_box) +{ + DzlSuggestion *suggestion; + + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + g_assert (DZL_IS_SUGGESTION_ROW (row)); + g_assert (GTK_IS_LIST_BOX (list_box)); + + suggestion = dzl_suggestion_row_get_suggestion (row); + g_signal_emit (self, signals [SUGGESTION_ACTIVATED], 0, suggestion); +} + +static void +dzl_suggestion_popover_list_box_row_selected (DzlSuggestionPopover *self, + DzlSuggestionRow *row, + GtkListBox *list_box) +{ + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + g_assert (!row || DZL_IS_SUGGESTION_ROW (row)); + g_assert (GTK_IS_LIST_BOX (list_box)); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SELECTED]); +} + +static void +dzl_suggestion_popover_destroy (GtkWidget *widget) +{ + DzlSuggestionPopover *self = (DzlSuggestionPopover *)widget; + + if (self->transient_for != NULL) + { + g_signal_handler_disconnect (self->transient_for, self->size_allocate_handler); + g_signal_handler_disconnect (self->transient_for, self->configure_event_handler); + g_signal_handler_disconnect (self->transient_for, self->delete_event_handler); + + self->size_allocate_handler = 0; + self->configure_event_handler = 0; + self->delete_event_handler = 0; + + self->transient_for = NULL; + } + + if (self->scroll_anim != NULL) + { + dzl_animation_stop (self->scroll_anim); + dzl_clear_weak_pointer (&self->scroll_anim); + } + + g_clear_object (&self->model); + + dzl_suggestion_popover_set_relative_to (self, NULL); + + GTK_WIDGET_CLASS (dzl_suggestion_popover_parent_class)->destroy (widget); +} + +static void +dzl_suggestion_popover_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSuggestionPopover *self = DZL_SUGGESTION_POPOVER (object); + + switch (prop_id) + { + case PROP_MODEL: + g_value_set_object (value, dzl_suggestion_popover_get_model (self)); + break; + + case PROP_RELATIVE_TO: + g_value_set_object (value, dzl_suggestion_popover_get_relative_to (self)); + break; + + case PROP_SELECTED: + g_value_set_object (value, dzl_suggestion_popover_get_selected (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_suggestion_popover_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSuggestionPopover *self = DZL_SUGGESTION_POPOVER (object); + + switch (prop_id) + { + case PROP_MODEL: + dzl_suggestion_popover_set_model (self, g_value_get_object (value)); + break; + + case PROP_RELATIVE_TO: + dzl_suggestion_popover_set_relative_to (self, g_value_get_object (value)); + break; + + case PROP_SELECTED: + dzl_suggestion_popover_set_selected (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_suggestion_popover_class_init (DzlSuggestionPopoverClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = dzl_suggestion_popover_get_property; + object_class->set_property = dzl_suggestion_popover_set_property; + + widget_class->destroy = dzl_suggestion_popover_destroy; + widget_class->hide = dzl_suggestion_popover_hide; + widget_class->screen_changed = dzl_suggestion_popover_screen_changed; + widget_class->realize = dzl_suggestion_popover_realize; + widget_class->show = dzl_suggestion_popover_show; + + properties [PROP_MODEL] = + g_param_spec_object ("model", + "Model", + "The model to be visualized", + DZL_TYPE_SUGGESTION, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_RELATIVE_TO] = + g_param_spec_object ("relative-to", + "Relative To", + "The widget to be relative to", + GTK_TYPE_WIDGET, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SELECTED] = + g_param_spec_object ("selected", + "Selected", + "The selected suggestion", + DZL_TYPE_SUGGESTION, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + signals [SUGGESTION_ACTIVATED] = + g_signal_new ("suggestion-activated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, DZL_TYPE_SUGGESTION); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-suggestion-popover.ui"); + gtk_widget_class_bind_template_child (widget_class, DzlSuggestionPopover, revealer); + gtk_widget_class_bind_template_child (widget_class, DzlSuggestionPopover, list_box); + gtk_widget_class_bind_template_child (widget_class, DzlSuggestionPopover, scrolled_window); + gtk_widget_class_bind_template_child (widget_class, DzlSuggestionPopover, top_box); + + gtk_widget_class_set_css_name (widget_class, "dzlsuggestionpopover"); + + g_type_ensure (DZL_TYPE_ELASTIC_BIN); + g_type_ensure (DZL_TYPE_LIST_BOX); + g_type_ensure (DZL_TYPE_SUGGESTION_ROW); +} + +static void +dzl_suggestion_popover_init (DzlSuggestionPopover *self) +{ + self->row_type = DZL_TYPE_SUGGESTION_ROW; + + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_window_set_type_hint (GTK_WINDOW (self), GDK_WINDOW_TYPE_HINT_COMBO); + gtk_window_set_skip_pager_hint (GTK_WINDOW (self), TRUE); + gtk_window_set_skip_taskbar_hint (GTK_WINDOW (self), TRUE); + gtk_window_set_decorated (GTK_WINDOW (self), FALSE); + gtk_window_set_resizable (GTK_WINDOW (self), FALSE); + + g_signal_connect_object (self->revealer, + "notify::child-revealed", + G_CALLBACK (dzl_suggestion_popover_notify_child_revealed), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (self->list_box, + "row-activated", + G_CALLBACK (dzl_suggestion_popover_list_box_row_activated), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (self->list_box, + "row-selected", + G_CALLBACK (dzl_suggestion_popover_list_box_row_selected), + self, + G_CONNECT_SWAPPED); + + dzl_list_box_set_recycle_max (self->list_box, 50); +} + +GtkWidget * +dzl_suggestion_popover_new (void) +{ + return g_object_new (DZL_TYPE_SUGGESTION_POPOVER, NULL); +} + +void +dzl_suggestion_popover_popup (DzlSuggestionPopover *self) +{ + guint duration = 250; + guint n_items; + + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + + if (self->model == NULL || 0 == (n_items = g_list_model_get_n_items (self->model))) + return; + + if (self->relative_to != NULL) + { + GdkDisplay *display; + GdkMonitor *monitor; + GdkWindow *window; + GtkAllocation alloc; + gint min_height; + gint nat_height; + + display = gtk_widget_get_display (GTK_WIDGET (self->relative_to)); + window = gtk_widget_get_window (GTK_WIDGET (self->relative_to)); + monitor = gdk_display_get_monitor_at_window (display, window); + + gtk_widget_get_preferred_height (GTK_WIDGET (self), &min_height, &nat_height); + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + duration = dzl_animation_calculate_duration (monitor, alloc.height, nat_height); + } + + gtk_widget_show (GTK_WIDGET (self)); + + gtk_revealer_set_transition_duration (self->revealer, duration); + gtk_revealer_set_reveal_child (self->revealer, TRUE); +} + +void +dzl_suggestion_popover_popdown (DzlSuggestionPopover *self) +{ + GtkAllocation alloc; + GdkDisplay *display; + GdkMonitor *monitor; + GdkWindow *window; + guint duration; + + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + + if (!gtk_widget_get_realized (GTK_WIDGET (self))) + return; + + display = gtk_widget_get_display (GTK_WIDGET (self->relative_to)); + window = gtk_widget_get_window (GTK_WIDGET (self->relative_to)); + monitor = gdk_display_get_monitor_at_window (display, window); + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + duration = dzl_animation_calculate_duration (monitor, alloc.height, 0); + + gtk_revealer_set_transition_duration (self->revealer, duration); + gtk_revealer_set_reveal_child (self->revealer, FALSE); +} + +static void +dzl_suggestion_popover_items_changed (DzlSuggestionPopover *self, + guint position, + guint removed, + guint added, + GListModel *model) +{ + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + g_assert (G_IS_LIST_MODEL (model)); + + if (g_list_model_get_n_items (model) == 0) + { + dzl_suggestion_popover_popdown (self); + return; + } + + /* + * If we are currently animating in the initial view of the popover, + * then we might need to cancel that animation and rely on the elastic + * bin for smooth resizing. + */ + if (gtk_revealer_get_reveal_child (self->revealer) && + !gtk_revealer_get_child_revealed (self->revealer) && + (removed || added)) + { + gtk_revealer_set_transition_duration (self->revealer, 0); + gtk_revealer_set_reveal_child (self->revealer, FALSE); + gtk_revealer_set_reveal_child (self->revealer, TRUE); + } +} + +static void +dzl_suggestion_popover_connect (DzlSuggestionPopover *self) +{ + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + + if (self->model == NULL) + return; + + dzl_list_box_set_model (self->list_box, self->model); + + self->items_changed_handler = + g_signal_connect_object (self->model, + "items-changed", + G_CALLBACK (dzl_suggestion_popover_items_changed), + self, + G_CONNECT_SWAPPED); + + if (g_list_model_get_n_items (self->model) == 0) + { + dzl_suggestion_popover_popdown (self); + return; + } + + /* select the first row */ + dzl_suggestion_popover_move_by (self, 1); + + /* If we started animating, cancel it to avoid such jarring movement. */ + if (self->scroll_anim != NULL) + { + dzl_animation_stop (self->scroll_anim); + dzl_clear_weak_pointer (&self->scroll_anim); + } + + gtk_adjustment_set_value (gtk_scrolled_window_get_vadjustment (self->scrolled_window), 0.0); +} + +static void +dzl_suggestion_popover_disconnect (DzlSuggestionPopover *self) +{ + g_assert (DZL_IS_SUGGESTION_POPOVER (self)); + + if (self->model == NULL) + return; + + g_signal_handler_disconnect (self->model, self->items_changed_handler); + self->items_changed_handler = 0; + + dzl_list_box_set_model (self->list_box, NULL); +} + +void +dzl_suggestion_popover_set_model (DzlSuggestionPopover *self, + GListModel *model) +{ + g_return_if_fail (DZL_IS_SUGGESTION_POPOVER (self)); + g_return_if_fail (!model || G_IS_LIST_MODEL (model)); + g_return_if_fail (!model || g_type_is_a (g_list_model_get_item_type (model), DZL_TYPE_SUGGESTION)); + + if (self->model != model) + { + if (self->model != NULL) + { + dzl_suggestion_popover_disconnect (self); + g_clear_object (&self->model); + } + + if (model != NULL) + { + self->model = g_object_ref (model); + dzl_suggestion_popover_connect (self); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]); + } +} + +/** + * dzl_suggestion_popover_get_model: + * @self: a #DzlSuggestionPopover + * + * Gets the model being visualized. + * + * Returns: (nullable) (transfer none): A #GListModel or %NULL. + */ +GListModel * +dzl_suggestion_popover_get_model (DzlSuggestionPopover *self) +{ + g_return_val_if_fail (DZL_IS_SUGGESTION_POPOVER (self), NULL); + + return self->model; +} + +static void +find_index_of_row (GtkWidget *widget, + gpointer user_data) +{ + struct { + GtkWidget *row; + gint index; + gint counter; + } *row_lookup = user_data; + + if (widget == row_lookup->row) + row_lookup->index = row_lookup->counter; + + row_lookup->counter++; +} + +void +dzl_suggestion_popover_move_by (DzlSuggestionPopover *self, + gint amount) +{ + GtkListBoxRow *row; + struct { + GtkWidget *row; + gint index; + gint counter; + } row_lookup; + + g_return_if_fail (DZL_IS_SUGGESTION_POPOVER (self)); + + if (NULL == (row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (self->list_box), 0))) + return; + + if (NULL == gtk_list_box_get_selected_row (GTK_LIST_BOX (self->list_box))) + { + dzl_suggestion_popover_select_row (self, row); + return; + } + + /* + * It would be nice if we have a bit better API to have control over + * this from GtkListBox. move-cursor isn't really sufficient for our + * control over position without updating the focus. + * + * We could look at doing focus redirection to the popover first, + * but that isn't exactly a clean solution either and suggests that + * we subclass the listbox too. + * + * This is really inefficient, but in general we won't have that + * many results, becuase showin the user a ton of results is not + * exactly useful. + * + * If we do decide to reuse this class in something autocompletion + * in a text editor, we'll want to restrategize (including avoiding + * the creation of unnecessary rows and row reuse). + */ + row = gtk_list_box_get_selected_row (GTK_LIST_BOX (self->list_box)); + + row_lookup.row = GTK_WIDGET (row); + row_lookup.counter = 0; + row_lookup.index = -1; + + gtk_container_foreach (GTK_CONTAINER (self->list_box), + find_index_of_row, + &row_lookup); + + row_lookup.index += amount; + row_lookup.index = CLAMP (row_lookup.index, -0, (gint)g_list_model_get_n_items (self->model) - 1); + + row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (self->list_box), row_lookup.index); + dzl_suggestion_popover_select_row (self, row); +} + +static void +find_suggestion_row (GtkWidget *widget, + gpointer user_data) +{ + DzlSuggestionRow *row = DZL_SUGGESTION_ROW (widget); + DzlSuggestion *suggestion = dzl_suggestion_row_get_suggestion (row); + struct { + DzlSuggestion *suggestion; + GtkListBoxRow **row; + } *lookup = user_data; + + if (suggestion == lookup->suggestion) + *lookup->row = GTK_LIST_BOX_ROW (row); +} + +void +dzl_suggestion_popover_set_selected (DzlSuggestionPopover *self, + DzlSuggestion *suggestion) +{ + GtkListBoxRow *row = NULL; + struct { + DzlSuggestion *suggestion; + GtkListBoxRow **row; + } lookup = { suggestion, &row }; + + g_return_if_fail (DZL_IS_SUGGESTION_POPOVER (self)); + g_return_if_fail (!suggestion || DZL_IS_SUGGESTION (suggestion)); + + if (suggestion == NULL) + row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (self->list_box), 0); + else + gtk_container_foreach (GTK_CONTAINER (self->list_box), find_suggestion_row, &lookup); + + if (row != NULL) + dzl_suggestion_popover_select_row (self, row); +} + +/** + * dzl_suggestion_popover_get_selected: + * @self: a #DzlSuggestionPopover + * + * Gets the currently selected suggestion. + * + * Returns: (transfer none) (nullable): An #DzlSuggestion or %NULL. + */ +DzlSuggestion * +dzl_suggestion_popover_get_selected (DzlSuggestionPopover *self) +{ + DzlSuggestionRow *row; + + g_return_val_if_fail (DZL_IS_SUGGESTION_POPOVER (self), NULL); + + row = DZL_SUGGESTION_ROW (gtk_list_box_get_selected_row (GTK_LIST_BOX (self->list_box))); + if (row != NULL) + return dzl_suggestion_row_get_suggestion (row); + + return NULL; +} + +void +dzl_suggestion_popover_activate_selected (DzlSuggestionPopover *self) +{ + DzlSuggestion *suggestion; + + g_return_if_fail (DZL_IS_SUGGESTION_POPOVER (self)); + + if (NULL != (suggestion = dzl_suggestion_popover_get_selected (self))) + g_signal_emit (self, signals [SUGGESTION_ACTIVATED], 0, suggestion); +} + +void +_dzl_suggestion_popover_set_max_height (DzlSuggestionPopover *self, + gint max_height) +{ + GdkWindow *window; + gint clip_height = 3000; /* something near 4k'ish */ + + g_return_if_fail (DZL_IS_SUGGESTION_POPOVER (self)); + + window = gtk_widget_get_window (GTK_WIDGET (self)); + + if (window != NULL) + { + GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (self)); + GdkMonitor *monitor = gdk_display_get_monitor_at_window (display, window); + + if (monitor != NULL) + { + GdkRectangle geom; + + gdk_monitor_get_geometry (monitor, &geom); + clip_height = geom.height; + } + } + + max_height = CLAMP (max_height, -1, clip_height); + + g_object_set (self->scrolled_window, + "max-content-height", max_height, + NULL); +} + +void +_dzl_suggestion_popover_adjust_margin (DzlSuggestionPopover *self, + GdkRectangle *area) +{ + GtkStyleContext *style_context; + GtkBorder margin; + + g_return_if_fail (DZL_IS_SUGGESTION_POPOVER (self)); + g_return_if_fail (area != NULL); + + /* + * This is our last chance to adjust the allocation. We need to subtract any + * margins needed by the box for theming. We style the shadow in the box so + * that we can do the show/hide with the revealer and have things come out + * looking correct. + */ + + style_context = gtk_widget_get_style_context (GTK_WIDGET (self->top_box)); + gtk_style_context_get_margin (style_context, + gtk_style_context_get_state (style_context), + &margin); + + area->x -= margin.right; + area->y -= margin.top; + area->width += margin.right + margin.left; + area->height += margin.top + margin.bottom; +} + diff --git a/src/suggestions/dzl-suggestion-popover.h b/src/suggestions/dzl-suggestion-popover.h new file mode 100644 index 0000000..62591b1 --- /dev/null +++ b/src/suggestions/dzl-suggestion-popover.h @@ -0,0 +1,63 @@ +/* dzl-suggestion-popover.h + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + + +#ifndef DZL_SUGGESTION_POPOVER_H +#define DZL_SUGGESTION_POPOVER_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SUGGESTION_POPOVER (dzl_suggestion_popover_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlSuggestionPopover, dzl_suggestion_popover, DZL, SUGGESTION_POPOVER, GtkWindow) + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_suggestion_popover_new (void); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_suggestion_popover_get_relative_to (DzlSuggestionPopover *self); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_popover_set_relative_to (DzlSuggestionPopover *self, + GtkWidget *widget); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_popover_popup (DzlSuggestionPopover *self); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_popover_popdown (DzlSuggestionPopover *self); +DZL_AVAILABLE_IN_ALL +GListModel *dzl_suggestion_popover_get_model (DzlSuggestionPopover *self); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_popover_set_model (DzlSuggestionPopover *self, + GListModel *model); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_popover_move_by (DzlSuggestionPopover *self, + gint amount); +DZL_AVAILABLE_IN_ALL +DzlSuggestion *dzl_suggestion_popover_get_selected (DzlSuggestionPopover *self); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_popover_set_selected (DzlSuggestionPopover *self, + DzlSuggestion *suggestion); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_popover_activate_selected (DzlSuggestionPopover *self); + +G_END_DECLS + +#endif /* DZL_SUGGESTION_POPOVER_H */ diff --git a/src/suggestions/dzl-suggestion-popover.ui b/src/suggestions/dzl-suggestion-popover.ui new file mode 100644 index 0000000..b0292c1 --- /dev/null +++ b/src/suggestions/dzl-suggestion-popover.ui @@ -0,0 +1,39 @@ + + + + diff --git a/src/suggestions/dzl-suggestion-private.h b/src/suggestions/dzl-suggestion-private.h new file mode 100644 index 0000000..62a59ca --- /dev/null +++ b/src/suggestions/dzl-suggestion-private.h @@ -0,0 +1,33 @@ +/* dzl-suggestion-private.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + + +#ifndef DZL_SUGGESTION_PRIVATE_H +#define DZL_SUGGESTION_PRIVATE_H + +#include "suggestions/dzl-suggestion-entry.h" +#include "suggestions/dzl-suggestion-popover.h" + +void _dzl_suggestion_entry_reposition (DzlSuggestionEntry *entry, + DzlSuggestionPopover *popover); +void _dzl_suggestion_popover_set_max_height (DzlSuggestionPopover *popover, + gint max_height); +void _dzl_suggestion_popover_adjust_margin (DzlSuggestionPopover *popover, + GdkRectangle *area); + +#endif /* DZL_SUGGESTION_PRIVATE_H */ diff --git a/src/suggestions/dzl-suggestion-row.c b/src/suggestions/dzl-suggestion-row.c new file mode 100644 index 0000000..e3fe72f --- /dev/null +++ b/src/suggestions/dzl-suggestion-row.c @@ -0,0 +1,209 @@ +/* dzl-suggestion-row.c + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-suggestion-row" + +#include "config.h" + +#include "dzl-suggestion-row.h" + +typedef struct +{ + DzlSuggestion *suggestion; + + GtkImage *image; + GtkLabel *title; + GtkLabel *separator; + GtkLabel *subtitle; +} DzlSuggestionRowPrivate; + +enum { + PROP_0, + PROP_SUGGESTION, + N_PROPS +}; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlSuggestionRow, dzl_suggestion_row, DZL_TYPE_LIST_BOX_ROW) + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_suggestion_row_disconnect (DzlSuggestionRow *self) +{ + DzlSuggestionRowPrivate *priv = dzl_suggestion_row_get_instance_private (self); + + g_return_if_fail (DZL_IS_SUGGESTION_ROW (self)); + + if (priv->suggestion == NULL) + return; + + g_object_set (priv->image, "icon-name", NULL, NULL); + gtk_label_set_label (priv->title, NULL); + gtk_label_set_label (priv->subtitle, NULL); +} + +static void +dzl_suggestion_row_connect (DzlSuggestionRow *self) +{ + DzlSuggestionRowPrivate *priv = dzl_suggestion_row_get_instance_private (self); + const gchar *icon_name; + const gchar *subtitle; + + g_return_if_fail (DZL_IS_SUGGESTION_ROW (self)); + g_return_if_fail (priv->suggestion != NULL); + + icon_name = dzl_suggestion_get_icon_name (priv->suggestion); + g_object_set (priv->image, "icon-name", icon_name, NULL); + + gtk_label_set_label (priv->title, dzl_suggestion_get_title (priv->suggestion)); + + subtitle = dzl_suggestion_get_subtitle (priv->suggestion); + gtk_label_set_label (priv->subtitle, subtitle); + gtk_widget_set_visible (GTK_WIDGET (priv->separator), !!subtitle); +} + +static void +dzl_suggestion_row_finalize (GObject *object) +{ + DzlSuggestionRow *self = (DzlSuggestionRow *)object; + DzlSuggestionRowPrivate *priv = dzl_suggestion_row_get_instance_private (self); + + g_clear_object (&priv->suggestion); + + G_OBJECT_CLASS (dzl_suggestion_row_parent_class)->finalize (object); +} + +static void +dzl_suggestion_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSuggestionRow *self = DZL_SUGGESTION_ROW (object); + + switch (prop_id) + { + case PROP_SUGGESTION: + g_value_set_object (value, dzl_suggestion_row_get_suggestion (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_suggestion_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSuggestionRow *self = DZL_SUGGESTION_ROW (object); + + switch (prop_id) + { + case PROP_SUGGESTION: + dzl_suggestion_row_set_suggestion (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_suggestion_row_class_init (DzlSuggestionRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_suggestion_row_finalize; + object_class->get_property = dzl_suggestion_row_get_property; + object_class->set_property = dzl_suggestion_row_set_property; + + properties [PROP_SUGGESTION] = + g_param_spec_object ("suggestion", + "Suggestion", + "The suggestion to display", + DZL_TYPE_SUGGESTION, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-suggestion-row.ui"); + gtk_widget_class_bind_template_child_private (widget_class, DzlSuggestionRow, image); + gtk_widget_class_bind_template_child_private (widget_class, DzlSuggestionRow, title); + gtk_widget_class_bind_template_child_private (widget_class, DzlSuggestionRow, subtitle); + gtk_widget_class_bind_template_child_private (widget_class, DzlSuggestionRow, separator); +} + +static void +dzl_suggestion_row_init (DzlSuggestionRow *self) +{ + GtkStyleContext *context; + + gtk_widget_init_template (GTK_WIDGET (self)); + + context = gtk_widget_get_style_context (GTK_WIDGET (self)); + gtk_style_context_add_class (context, "suggestion"); +} + +/** + * dzl_suggestion_row_get_suggestion: + * @self: a #DzlSuggestionRow + * + * Gets the suggestion to be displayed. + * + * Returns: (transfer none): An #DzlSuggestion + */ +DzlSuggestion * +dzl_suggestion_row_get_suggestion (DzlSuggestionRow *self) +{ + DzlSuggestionRowPrivate *priv = dzl_suggestion_row_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SUGGESTION_ROW (self), NULL); + + return priv->suggestion; +} + +void +dzl_suggestion_row_set_suggestion (DzlSuggestionRow *self, + DzlSuggestion *suggestion) +{ + DzlSuggestionRowPrivate *priv = dzl_suggestion_row_get_instance_private (self); + + g_return_if_fail (DZL_IS_SUGGESTION_ROW (self)); + g_return_if_fail (!suggestion || DZL_IS_SUGGESTION (suggestion)); + + if (priv->suggestion != suggestion) + { + if (priv->suggestion != NULL) + { + dzl_suggestion_row_disconnect (self); + g_clear_object (&priv->suggestion); + } + + if (suggestion != NULL) + { + priv->suggestion = g_object_ref (suggestion); + dzl_suggestion_row_connect (self); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SUGGESTION]); + } +} diff --git a/src/suggestions/dzl-suggestion-row.h b/src/suggestions/dzl-suggestion-row.h new file mode 100644 index 0000000..04e0108 --- /dev/null +++ b/src/suggestions/dzl-suggestion-row.h @@ -0,0 +1,56 @@ +/* dzl-suggestion-row.h + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#ifndef DZL_SUGGESTION_ROW_H +#define DZL_SUGGESTION_ROW_H + +#include + +#include "dzl-version-macros.h" + +#include "suggestions/dzl-suggestion.h" +#include "widgets/dzl-list-box-row.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SUGGESTION_ROW (dzl_suggestion_row_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlSuggestionRow, dzl_suggestion_row, DZL, SUGGESTION_ROW, DzlListBoxRow) + +struct _DzlSuggestionRowClass +{ + DzlListBoxRowClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_suggestion_row_new (void); +DZL_AVAILABLE_IN_ALL +DzlSuggestion *dzl_suggestion_row_get_suggestion (DzlSuggestionRow *self); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_row_set_suggestion (DzlSuggestionRow *self, + DzlSuggestion *suggestion); + +G_END_DECLS + +#endif /* DZL_SUGGESTION_ROW_H */ diff --git a/src/suggestions/dzl-suggestion-row.ui b/src/suggestions/dzl-suggestion-row.ui new file mode 100644 index 0000000..f2694e3 --- /dev/null +++ b/src/suggestions/dzl-suggestion-row.ui @@ -0,0 +1,54 @@ + + + + diff --git a/src/suggestions/dzl-suggestion.c b/src/suggestions/dzl-suggestion.c new file mode 100644 index 0000000..5d42f43 --- /dev/null +++ b/src/suggestions/dzl-suggestion.c @@ -0,0 +1,357 @@ +/* dzl-suggestion.c + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-suggestion" + +#include "config.h" + +#include "dzl-suggestion.h" + +typedef struct +{ + gchar *title; + gchar *subtitle; + gchar *id; + + /* interned string */ + const gchar *icon_name; +} DzlSuggestionPrivate; + +enum { + PROP_0, + PROP_ICON_NAME, + PROP_ID, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + +enum { + REPLACE_TYPED_TEXT, + SUGGEST_SUFFIX, + N_SIGNALS +}; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlSuggestion, dzl_suggestion, G_TYPE_OBJECT) + +static GParamSpec *properties [N_PROPS]; +static guint signals [N_SIGNALS]; + +static void +dzl_suggestion_finalize (GObject *object) +{ + DzlSuggestion *self = (DzlSuggestion *)object; + DzlSuggestionPrivate *priv = dzl_suggestion_get_instance_private (self); + + priv->icon_name = NULL; + + g_clear_pointer (&priv->title, g_free); + g_clear_pointer (&priv->subtitle, g_free); + g_clear_pointer (&priv->id, g_free); + + G_OBJECT_CLASS (dzl_suggestion_parent_class)->finalize (object); +} + +static void +dzl_suggestion_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSuggestion *self = DZL_SUGGESTION (object); + + switch (prop_id) + { + case PROP_ID: + g_value_set_string (value, dzl_suggestion_get_id (self)); + break; + + case PROP_ICON_NAME: + g_value_set_static_string (value, dzl_suggestion_get_icon_name (self)); + break; + + case PROP_TITLE: + g_value_set_string (value, dzl_suggestion_get_title (self)); + break; + + case PROP_SUBTITLE: + g_value_set_string (value, dzl_suggestion_get_subtitle (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_suggestion_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSuggestion *self = DZL_SUGGESTION (object); + + switch (prop_id) + { + case PROP_ICON_NAME: + dzl_suggestion_set_icon_name (self, g_value_get_string (value)); + break; + + case PROP_ID: + dzl_suggestion_set_id (self, g_value_get_string (value)); + break; + + case PROP_TITLE: + dzl_suggestion_set_title (self, g_value_get_string (value)); + break; + + case PROP_SUBTITLE: + dzl_suggestion_set_subtitle (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_suggestion_class_init (DzlSuggestionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_suggestion_finalize; + object_class->get_property = dzl_suggestion_get_property; + object_class->set_property = dzl_suggestion_set_property; + + properties [PROP_ID] = + g_param_spec_string ("id", + "Id", + "The suggestion identifier", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "Icon Name", + "The name of the icon to display", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "The title of the suggestion", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SUBTITLE] = + g_param_spec_string ("subtitle", + "Subtitle", + "The subtitle of the suggestion", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + signals [REPLACE_TYPED_TEXT] = + g_signal_new ("replace-typed-text", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlSuggestionClass, replace_typed_text), + g_signal_accumulator_first_wins, NULL, NULL, + G_TYPE_STRING, 1, G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE); + + signals [SUGGEST_SUFFIX] = + g_signal_new ("suggest-suffix", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlSuggestionClass, suggest_suffix), + g_signal_accumulator_first_wins, NULL, NULL, + G_TYPE_STRING, 1, G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE); +} + +static void +dzl_suggestion_init (DzlSuggestion *self) +{ +} + +const gchar * +dzl_suggestion_get_id (DzlSuggestion *self) +{ + DzlSuggestionPrivate *priv = dzl_suggestion_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SUGGESTION (self), NULL); + + return priv->id; +} + +const gchar * +dzl_suggestion_get_icon_name (DzlSuggestion *self) +{ + DzlSuggestionPrivate *priv = dzl_suggestion_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SUGGESTION (self), NULL); + + return priv->icon_name; +} + +const gchar * +dzl_suggestion_get_title (DzlSuggestion *self) +{ + DzlSuggestionPrivate *priv = dzl_suggestion_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SUGGESTION (self), NULL); + + return priv->title; +} + +const gchar * +dzl_suggestion_get_subtitle (DzlSuggestion *self) +{ + DzlSuggestionPrivate *priv = dzl_suggestion_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SUGGESTION (self), NULL); + + return priv->subtitle; +} + +void +dzl_suggestion_set_icon_name (DzlSuggestion *self, + const gchar *icon_name) +{ + DzlSuggestionPrivate *priv = dzl_suggestion_get_instance_private (self); + + g_return_if_fail (DZL_IS_SUGGESTION (self)); + + icon_name = g_intern_string (icon_name); + + if (priv->icon_name != icon_name) + { + priv->icon_name = icon_name; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]); + } +} + +void +dzl_suggestion_set_id (DzlSuggestion *self, + const gchar *id) +{ + DzlSuggestionPrivate *priv = dzl_suggestion_get_instance_private (self); + + g_return_if_fail (DZL_IS_SUGGESTION (self)); + + if (g_strcmp0 (priv->id, id) != 0) + { + g_free (priv->id); + priv->id = g_strdup (id); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]); + } +} + +void +dzl_suggestion_set_title (DzlSuggestion *self, + const gchar *title) +{ + DzlSuggestionPrivate *priv = dzl_suggestion_get_instance_private (self); + + g_return_if_fail (DZL_IS_SUGGESTION (self)); + + if (g_strcmp0 (priv->title, title) != 0) + { + g_free (priv->title); + priv->title = g_strdup (title); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]); + } +} + +void +dzl_suggestion_set_subtitle (DzlSuggestion *self, + const gchar *subtitle) +{ + DzlSuggestionPrivate *priv = dzl_suggestion_get_instance_private (self); + + g_return_if_fail (DZL_IS_SUGGESTION (self)); + + if (g_strcmp0 (priv->subtitle, subtitle) != 0) + { + g_free (priv->subtitle); + priv->subtitle = g_strdup (subtitle); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SUBTITLE]); + } +} + +/** + * dzl_suggestion_suggest_suffix: + * @self: a #DzlSuggestion + * @typed_text: The user entered text + * + * This function requests potential text to append to @typed_text to make it + * more clear to the user what they will be activating by selecting this + * suggestion. For example, if they start typing "gno", a potential suggested + * suffix might be "me.org" to create "gnome.org". + * + * Returns: (transfer full) (nullable): Suffix to append to @typed_text + * or %NULL to leave it unchanged. + */ +gchar * +dzl_suggestion_suggest_suffix (DzlSuggestion *self, + const gchar *typed_text) +{ + gchar *ret = NULL; + + g_return_val_if_fail (DZL_IS_SUGGESTION (self), NULL); + g_return_val_if_fail (typed_text != NULL, NULL); + + g_signal_emit (self, signals [SUGGEST_SUFFIX], 0, typed_text, &ret); + + return ret; +} + +DzlSuggestion * +dzl_suggestion_new (void) +{ + return g_object_new (DZL_TYPE_SUGGESTION, NULL); +} + +/** + * dzl_suggestion_replace_typed_text: + * @self: An #DzlSuggestion + * @typed_text: the text that was typed into the entry + * + * This function is meant to be used to replace the text in the entry with text + * that represents the suggestion most accurately. This happens when the user + * presses tab while typing a suggestion. For example, if typing "gno" in the + * entry, you might have a suggest_suffix of "me.org" so that the user sees + * "gnome.org". But the replace_typed_text might include more data such as + * "https://gnome.org" as it more closely represents the suggestion. + * + * Returns: (transfer full) (nullable): The replacement text to insert into + * the entry when "tab" is pressed to complete the insertion. + */ +gchar * +dzl_suggestion_replace_typed_text (DzlSuggestion *self, + const gchar *typed_text) +{ + gchar *ret = NULL; + + g_return_val_if_fail (DZL_IS_SUGGESTION (self), NULL); + + g_signal_emit (self, signals [REPLACE_TYPED_TEXT], 0, typed_text, &ret); + + return ret; +} diff --git a/src/suggestions/dzl-suggestion.h b/src/suggestions/dzl-suggestion.h new file mode 100644 index 0000000..78d16a3 --- /dev/null +++ b/src/suggestions/dzl-suggestion.h @@ -0,0 +1,80 @@ +/* dzl-suggestion.h + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + + +#ifndef DZL_SUGGESTION_H +#define DZL_SUGGESTION_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SUGGESTION (dzl_suggestion_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlSuggestion, dzl_suggestion, DZL, SUGGESTION, GObject) + +struct _DzlSuggestionClass +{ + GObjectClass parent_class; + + gchar *(*suggest_suffix) (DzlSuggestion *self, + const gchar *typed_text); + gchar *(*replace_typed_text) (DzlSuggestion *self, + const gchar *typed_text); + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; +}; + +DZL_AVAILABLE_IN_ALL +DzlSuggestion *dzl_suggestion_new (void); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_suggestion_get_id (DzlSuggestion *self); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_set_id (DzlSuggestion *self, + const gchar *id); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_suggestion_get_icon_name (DzlSuggestion *self); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_set_icon_name (DzlSuggestion *self, + const gchar *icon_name); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_suggestion_get_title (DzlSuggestion *self); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_set_title (DzlSuggestion *self, + const gchar *title); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_suggestion_get_subtitle (DzlSuggestion *self); +DZL_AVAILABLE_IN_ALL +void dzl_suggestion_set_subtitle (DzlSuggestion *self, + const gchar *subtitle); +DZL_AVAILABLE_IN_ALL +gchar *dzl_suggestion_suggest_suffix (DzlSuggestion *self, + const gchar *typed_text); +DZL_AVAILABLE_IN_ALL +gchar *dzl_suggestion_replace_typed_text (DzlSuggestion *self, + const gchar *typed_text); + +G_END_DECLS + +#endif /* DZL_SUGGESTION_H */ diff --git a/src/suggestions/meson.build b/src/suggestions/meson.build new file mode 100644 index 0000000..db91b61 --- /dev/null +++ b/src/suggestions/meson.build @@ -0,0 +1,20 @@ +suggestions_headers = [ + 'dzl-suggestion-entry-buffer.h', + 'dzl-suggestion-entry.h', + 'dzl-suggestion-popover.h', + 'dzl-suggestion-row.h', + 'dzl-suggestion.h', +] + +suggestions_sources = [ + 'dzl-suggestion-entry-buffer.c', + 'dzl-suggestion-entry.c', + 'dzl-suggestion-popover.c', + 'dzl-suggestion-row.c', + 'dzl-suggestion.c', +] + +libdazzle_public_headers += files(suggestions_headers) +libdazzle_public_sources += files(suggestions_sources) + +install_headers(suggestions_headers, subdir: join_paths(libdazzle_header_subdir, 'suggestions')) diff --git a/src/theming/dzl-css-provider.c b/src/theming/dzl-css-provider.c new file mode 100644 index 0000000..a825601 --- /dev/null +++ b/src/theming/dzl-css-provider.c @@ -0,0 +1,331 @@ +/* dzl-css-provider.c + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-css-provider" + +#include "config.h" + +#include + +#include "theming/dzl-css-provider.h" + +struct _DzlCssProvider +{ + GtkCssProvider parent_instance; + gchar *base_path; + guint queued_update; +}; + +G_DEFINE_TYPE (DzlCssProvider, dzl_css_provider, GTK_TYPE_CSS_PROVIDER) + +enum { + PROP_0, + PROP_BASE_PATH, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +GtkCssProvider * +dzl_css_provider_new (const gchar *base_path) +{ + return g_object_new (DZL_TYPE_CSS_PROVIDER, + "base-path", base_path, + NULL); +} + +static gboolean +resource_exists (const gchar *resource_path) +{ + g_assert (resource_path != NULL); + + if (g_str_has_prefix (resource_path, "resource://")) + { + gsize len = 0; + guint32 flags = 0; + + resource_path += strlen ("resource://"); + + return g_resources_get_info (resource_path, G_RESOURCE_LOOKUP_FLAGS_NONE, &len, &flags, NULL); + } + + return g_file_test (resource_path, G_FILE_TEST_IS_REGULAR); +} + +static void +load_resource (DzlCssProvider *self, + const gchar *resource_path) +{ + g_assert (DZL_IS_CSS_PROVIDER (self)); + g_assert (resource_path != NULL); + + if (g_str_has_prefix (resource_path, "resource://")) + { + resource_path += strlen ("resource://"); + gtk_css_provider_load_from_resource (GTK_CSS_PROVIDER (self), resource_path); + } + else + { + g_autoptr(GError) error = NULL; + + if (!gtk_css_provider_load_from_path (GTK_CSS_PROVIDER (self), resource_path, &error)) + g_warning ("%s", error->message); + } + +} + +static void +dzl_css_provider_update (DzlCssProvider *self) +{ + g_autofree gchar *theme_name = NULL; + g_autofree gchar *resource_path = NULL; + GtkSettings *settings; + gboolean prefer_dark_theme = FALSE; + + g_assert (DZL_IS_CSS_PROVIDER (self)); + + settings = gtk_settings_get_default (); + theme_name = g_strdup (g_getenv ("GTK_THEME")); + + if (theme_name != NULL) + { + char *p; + + /* Theme variants are specified with the syntax + * ":" e.g. "Adwaita:dark" */ + if (NULL != (p = strrchr (theme_name, ':'))) + { + *p = '\0'; + p++; + prefer_dark_theme = g_strcmp0 (p, "dark") == 0; + } + } + else + { + g_object_get (settings, + "gtk-theme-name", &theme_name, + "gtk-application-prefer-dark-theme", &prefer_dark_theme, + NULL); + } + + /* First check with full path to theme+variant */ + resource_path = g_strdup_printf ("%s/%s%s.css", + self->base_path, + theme_name, prefer_dark_theme ? "-dark" : ""); + + if (!resource_exists (resource_path)) + { + /* Now try without the theme variant */ + g_free (resource_path); + resource_path = g_strdup_printf ("%s/%s.css", self->base_path, theme_name); + + /* Now fallback to shared styling */ + if (!resource_exists (resource_path)) + { + g_free (resource_path); + resource_path = g_strdup_printf ("%s/shared.css", self->base_path); + + if (!resource_exists (resource_path)) + return; + } + } + + g_debug ("Loading css overrides \"%s\"", resource_path); + + load_resource (self, resource_path); +} + +static gboolean +dzl_css_provider_do_update (gpointer user_data) +{ + DzlCssProvider *self = user_data; + + g_assert (DZL_IS_CSS_PROVIDER (self)); + + self->queued_update = 0; + dzl_css_provider_update (self); + + return G_SOURCE_REMOVE; +} + +static void +dzl_css_provider_queue_update (DzlCssProvider *self) +{ + g_assert (DZL_IS_CSS_PROVIDER (self)); + + if (self->queued_update == 0) + self->queued_update = g_timeout_add (0, dzl_css_provider_do_update, self); +} + +static void +dzl_css_provider__settings_notify_gtk_theme_name (DzlCssProvider *self, + GParamSpec *pspec, + GtkSettings *settings) +{ + g_assert (DZL_IS_CSS_PROVIDER (self)); + + dzl_css_provider_queue_update (self); +} + +static void +dzl_css_provider__settings_notify_gtk_application_prefer_dark_theme (DzlCssProvider *self, + GParamSpec *pspec, + GtkSettings *settings) +{ + g_assert (DZL_IS_CSS_PROVIDER (self)); + + dzl_css_provider_queue_update (self); +} + +static void +dzl_css_provider_parsing_error (GtkCssProvider *provider, + GtkCssSection *section, + const GError *error) +{ + g_autofree gchar *uri = NULL; + GFile *file; + guint line = 0; + guint line_offset = 0; + + g_assert (DZL_IS_CSS_PROVIDER (provider)); + g_assert (error != NULL); + + if (section != NULL) + { + file = gtk_css_section_get_file (section); + uri = g_file_get_uri (file); + line = gtk_css_section_get_start_line (section); + line_offset = gtk_css_section_get_start_position (section); + g_warning ("Parsing Error: %s @ %u:%u: %s", uri, line, line_offset, error->message); + } + else + { + g_warning ("%s", error->message); + } +} + +static void +dzl_css_provider_constructed (GObject *object) +{ + DzlCssProvider *self = (DzlCssProvider *)object; + GtkSettings *settings; + + G_OBJECT_CLASS (dzl_css_provider_parent_class)->constructed (object); + + settings = gtk_settings_get_default (); + + g_signal_connect_object (settings, + "notify::gtk-theme-name", + G_CALLBACK (dzl_css_provider__settings_notify_gtk_theme_name), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (settings, + "notify::gtk-application-prefer-dark-theme", + G_CALLBACK (dzl_css_provider__settings_notify_gtk_application_prefer_dark_theme), + self, + G_CONNECT_SWAPPED); + + dzl_css_provider_update (self); +} + +static void +dzl_css_provider_finalize (GObject *object) +{ + DzlCssProvider *self = (DzlCssProvider *)object; + + g_clear_pointer (&self->base_path, g_free); + + G_OBJECT_CLASS (dzl_css_provider_parent_class)->finalize (object); +} + +static void +dzl_css_provider_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlCssProvider *self = DZL_CSS_PROVIDER(object); + + switch (prop_id) + { + case PROP_BASE_PATH: + g_value_set_string (value, self->base_path); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + } +} + +static void +dzl_css_provider_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlCssProvider *self = DZL_CSS_PROVIDER(object); + + switch (prop_id) + { + case PROP_BASE_PATH: + { + const gchar *str = g_value_get_string (value); + gsize len = str ? strlen (str) : 0; + + /* Ignore trailing slash to simplify building paths */ + if (str && len && str[len-1] == '/') + self->base_path = g_strndup (str, len - 1); + else + self->base_path = g_strdup (str); + + break; + } + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + } +} + +static void +dzl_css_provider_class_init (DzlCssProviderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkCssProviderClass *provider_class = GTK_CSS_PROVIDER_CLASS (klass); + + object_class->constructed = dzl_css_provider_constructed; + object_class->finalize = dzl_css_provider_finalize; + object_class->get_property = dzl_css_provider_get_property; + object_class->set_property = dzl_css_provider_set_property; + + provider_class->parsing_error = dzl_css_provider_parsing_error; + + properties [PROP_BASE_PATH] = + g_param_spec_string ("base-path", + "Base Path", + "The base resource path to discover themes", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_css_provider_init (DzlCssProvider *self) +{ +} diff --git a/src/theming/dzl-css-provider.h b/src/theming/dzl-css-provider.h new file mode 100644 index 0000000..1e6b1fc --- /dev/null +++ b/src/theming/dzl-css-provider.h @@ -0,0 +1,38 @@ +/* dzl-css-provider.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_CSS_PROVIDER_H +#define DZL_CSS_PROVIDER_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_CSS_PROVIDER (dzl_css_provider_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlCssProvider, dzl_css_provider, DZL, CSS_PROVIDER, GtkCssProvider) + +DZL_AVAILABLE_IN_ALL +GtkCssProvider *dzl_css_provider_new (const gchar *base_path); + +G_END_DECLS + +#endif /* DZL_CSS_PROVIDER_H */ diff --git a/src/theming/dzl-theme-manager.c b/src/theming/dzl-theme-manager.c new file mode 100644 index 0000000..aa28dd1 --- /dev/null +++ b/src/theming/dzl-theme-manager.c @@ -0,0 +1,195 @@ +/* dzl-theme-manager.c + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-theme-manager" + +#include "config.h" + +#include + +#include "theming/dzl-css-provider.h" +#include "theming/dzl-theme-manager.h" + +struct _DzlThemeManager +{ + GObject parent_instance; + GHashTable *providers_by_path; +}; + +G_DEFINE_TYPE (DzlThemeManager, dzl_theme_manager, G_TYPE_OBJECT) + +static void +dzl_theme_manager_finalize (GObject *object) +{ + DzlThemeManager *self = (DzlThemeManager *)object; + + g_clear_pointer (&self->providers_by_path, g_hash_table_unref); + + G_OBJECT_CLASS (dzl_theme_manager_parent_class)->finalize (object); +} + +static void +dzl_theme_manager_class_init (DzlThemeManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_theme_manager_finalize; +} + +static void +dzl_theme_manager_init (DzlThemeManager *self) +{ + self->providers_by_path = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + g_object_unref); +} + +DzlThemeManager * +dzl_theme_manager_new (void) +{ + return g_object_new (DZL_TYPE_THEME_MANAGER, NULL); +} + +static gboolean +has_child_resources (const gchar *path) +{ + g_auto(GStrv) children = NULL; + + if (g_str_has_prefix (path, "resource://")) + path += strlen ("resource://"); + + children = g_resources_enumerate_children (path, 0, NULL); + + return children != NULL && children[0] != NULL; +} + +/** + * dzl_theme_manager_add_resources: + * @self: a #DzlThemeManager + * @resource_path: A path to a #GResources directory + * + * This will automatically register resources found within @resource_path. + * + * If @resource_path starts with "resource://", embedded #GResources will be + * used to locate the theme files. Otherwise, @resource_path is expected to be + * a path on disk that may or may not exist. + * + * If the @resource_path contains a directory named "themes", that directory + * will be traversed for files matching the theme name and variant. For + * example, if using the Adwaita theme, "themes/Adwaita.css" will be loaded. If + * the dark variant is being used, "themes/Adwaita-dark.css" will be loaeded. If + * no matching theme file is located, "themes/shared.css" will be loaded. + * + * When the current theme changes, the CSS will be reloaded to adapt. + * + * The "icons" sub-directory will be used to locate icon themes. + */ +void +dzl_theme_manager_add_resources (DzlThemeManager *self, + const gchar *resource_path) +{ + g_autoptr(GtkCssProvider) provider = NULL; + g_autofree gchar *css_dir = NULL; + g_autofree gchar *icons_dir = NULL; + const gchar *real_path = resource_path; + GtkIconTheme *theme; + + g_return_if_fail (DZL_IS_THEME_MANAGER (self)); + g_return_if_fail (resource_path != NULL); + + theme = gtk_icon_theme_get_default (); + + if (g_str_has_prefix (real_path, "resource://")) + real_path += strlen ("resource://"); + + /* + * Create a CSS provider that will load the proper variant based on the + * current application theme, using @resource_path/css as the base directory + * to locate theming files. + */ + css_dir = g_build_path ("/", resource_path, "themes/", NULL); + g_debug ("Including CSS overrides from %s", css_dir); + + if (has_child_resources (css_dir)) + { + provider = dzl_css_provider_new (css_dir); + g_hash_table_insert (self->providers_by_path, g_strdup (resource_path), g_object_ref (provider)); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + /* + * Add the icons sub-directory so that Gtk can locate the themed + * icons (svg, png, etc). + */ + icons_dir = g_build_path ("/", real_path, "icons/", NULL); + g_debug ("Loading icon resources from %s", icons_dir); + if (!g_str_equal (real_path, resource_path)) + { + g_auto(GStrv) children = NULL; + + /* Okay, this is a resource-based path. Make sure the + * path contains children so we don't slow down the + * theme loading code with tons of useless directories. + */ + children = g_resources_enumerate_children (icons_dir, 0, NULL); + if (children != NULL && children[0] != NULL) + gtk_icon_theme_add_resource_path (theme, icons_dir); + } + else + { + /* Make sure the directory exists so that we don't needlessly + * slow down the icon loading paths. + */ + if (g_file_test (icons_dir, G_FILE_TEST_IS_DIR)) + gtk_icon_theme_append_search_path (theme, icons_dir); + } +} + +/** + * dzl_theme_manager_remove_resources: + * @self: a #DzlThemeManager + * @resource_path: A previously registered resources path + * + * This removes the CSS providers that were registered using @resource_path. + * + * You must have previously called dzl_theme_manager_add_resources() for + * this function to do anything. + * + * Since icons cannot be unloaded, previously loaded icons will continue to + * be available even after calling this function. + */ +void +dzl_theme_manager_remove_resources (DzlThemeManager *self, + const gchar *resource_path) +{ + GtkStyleProvider *provider; + + g_return_if_fail (DZL_IS_THEME_MANAGER (self)); + g_return_if_fail (resource_path != NULL); + g_return_if_fail (g_hash_table_contains (self->providers_by_path, resource_path)); + + if (NULL != (provider = g_hash_table_lookup (self->providers_by_path, resource_path))) + { + g_debug ("Removing CSS overrides from %s", resource_path); + gtk_style_context_remove_provider_for_screen (gdk_screen_get_default (), provider); + g_hash_table_remove (self->providers_by_path, resource_path); + } +} diff --git a/src/theming/dzl-theme-manager.h b/src/theming/dzl-theme-manager.h new file mode 100644 index 0000000..c0d5256 --- /dev/null +++ b/src/theming/dzl-theme-manager.h @@ -0,0 +1,44 @@ +/* dzl-theme-manager.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_THEME_MANAGER_H +#define DZL_THEME_MANAGER_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_THEME_MANAGER (dzl_theme_manager_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlThemeManager, dzl_theme_manager, DZL, THEME_MANAGER, GObject) + +DZL_AVAILABLE_IN_ALL +DzlThemeManager *dzl_theme_manager_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_theme_manager_add_resources (DzlThemeManager *self, + const gchar *resource_path); +DZL_AVAILABLE_IN_ALL +void dzl_theme_manager_remove_resources (DzlThemeManager *self, + const gchar *resource_path); + +G_END_DECLS + +#endif /* DZL_THEME_MANAGER_H */ diff --git a/src/theming/meson.build b/src/theming/meson.build new file mode 100644 index 0000000..f9ebe32 --- /dev/null +++ b/src/theming/meson.build @@ -0,0 +1,14 @@ +theming_headers = [ + 'dzl-css-provider.h', + 'dzl-theme-manager.h', +] + +theming_sources = [ + 'dzl-css-provider.c', + 'dzl-theme-manager.c', +] + +libdazzle_public_headers += files(theming_headers) +libdazzle_public_sources += files(theming_sources) + +install_headers(theming_headers, subdir: join_paths(libdazzle_header_subdir, 'theming')) diff --git a/src/tree/dzl-list-store-adapter.c b/src/tree/dzl-list-store-adapter.c new file mode 100644 index 0000000..6590b18 --- /dev/null +++ b/src/tree/dzl-list-store-adapter.c @@ -0,0 +1,415 @@ +/* dzl-list-store-adapter.c + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-list-store-adapter" + +#include "config.h" + +#include "bindings/dzl-signal-group.h" +#include "tree/dzl-list-store-adapter.h" + +typedef struct +{ + DzlSignalGroup *signals; + GListModel *model; + gint length; + GType type; +} DzlListStoreAdapterPrivate; + +enum { + PROP_0, + PROP_MODEL, + N_PROPS +}; + +static void tree_model_iface_init (GtkTreeModelIface *iface); + +G_DEFINE_TYPE_WITH_CODE (DzlListStoreAdapter, dzl_list_store_adapter, G_TYPE_OBJECT, + G_ADD_PRIVATE (DzlListStoreAdapter) + G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, tree_model_iface_init)) + +static GParamSpec *properties [N_PROPS]; + +static GtkTreeModelFlags +dzl_list_store_adapter_get_flags (GtkTreeModel *model) +{ + return GTK_TREE_MODEL_ITERS_PERSIST | GTK_TREE_MODEL_LIST_ONLY; +} + +static gint +dzl_list_store_adapter_get_n_columns (GtkTreeModel *model) +{ + return 1; +} + +static GType +dzl_list_store_adapter_get_column_type (GtkTreeModel *model, + gint column) +{ + DzlListStoreAdapter *self = DZL_LIST_STORE_ADAPTER (model); + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + + if (column == 0) + return priv->type; + + return G_TYPE_INVALID; +} + +static gboolean +dzl_list_store_adapter_get_iter (GtkTreeModel *model, + GtkTreeIter *iter, + GtkTreePath *path) +{ + DzlListStoreAdapter *self = (DzlListStoreAdapter *)model; + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + gint pos; + + g_assert (DZL_IS_LIST_STORE_ADAPTER (self)); + g_assert (iter != NULL); + g_assert (path != NULL); + + if (gtk_tree_path_get_depth (path) != 1) + return FALSE; + + pos = gtk_tree_path_get_indices (path) [0]; + + if (pos >= priv->length) + return FALSE; + + iter->user_data = GINT_TO_POINTER (pos); + + return TRUE; +} + +static GtkTreePath * +dzl_list_store_adapter_get_path (GtkTreeModel *model, + GtkTreeIter *iter) +{ + return gtk_tree_path_new_from_indices (GPOINTER_TO_INT (iter->user_data), -1); +} + +static void +dzl_list_store_adapter_get_value (GtkTreeModel *model, + GtkTreeIter *iter, + gint column, + GValue *value) +{ + DzlListStoreAdapter *self = DZL_LIST_STORE_ADAPTER (model); + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + + g_value_init (value, priv->type); + g_value_take_object (value, + g_list_model_get_item (priv->model, + GPOINTER_TO_INT (iter->user_data))); +} + +static gboolean +dzl_list_store_adapter_iter_next (GtkTreeModel *model, + GtkTreeIter *iter) +{ + DzlListStoreAdapter *self = DZL_LIST_STORE_ADAPTER (model); + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + gint pos = GPOINTER_TO_INT (iter->user_data) + 1; + + iter->user_data = GINT_TO_POINTER (pos); + + return pos < priv->length; +} + +static gboolean +dzl_list_store_adapter_iter_previous (GtkTreeModel *model, + GtkTreeIter *iter) +{ + gint pos = GPOINTER_TO_INT (iter->user_data) - 1; + + iter->user_data = GINT_TO_POINTER (pos); + + return pos >= 0; +} + +static gboolean +dzl_list_store_adapter_iter_children (GtkTreeModel *model, + GtkTreeIter *iter, + GtkTreeIter *parent) +{ + if (parent != NULL) + return FALSE; + + iter->user_data = NULL; + + return TRUE; +} + +static gboolean +dzl_list_store_adapter_iter_has_child (GtkTreeModel *model, + GtkTreeIter *iter) +{ + return iter == NULL; +} + +static gint +dzl_list_store_adapter_iter_n_children (GtkTreeModel *model, + GtkTreeIter *iter) +{ + DzlListStoreAdapter *self = DZL_LIST_STORE_ADAPTER (model); + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + + if (iter == NULL) + return priv->length; + + return 0; +} + +static gboolean +dzl_list_store_adapter_iter_parent (GtkTreeModel *model, + GtkTreeIter *iter, + GtkTreeIter *child) +{ + return FALSE; +} + +static gboolean +dzl_list_store_adapter_iter_nth_child (GtkTreeModel *model, + GtkTreeIter *iter, + GtkTreeIter *parent, + gint nth) +{ + DzlListStoreAdapter *self = DZL_LIST_STORE_ADAPTER (model); + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + + if (parent == NULL && nth < priv->length) + { + iter->user_data = GINT_TO_POINTER (nth); + return TRUE; + } + + return FALSE; +} + +static void +tree_model_iface_init (GtkTreeModelIface *iface) +{ + iface->get_flags = dzl_list_store_adapter_get_flags; + iface->get_n_columns = dzl_list_store_adapter_get_n_columns; + iface->get_column_type = dzl_list_store_adapter_get_column_type; + iface->get_iter = dzl_list_store_adapter_get_iter; + iface->get_path = dzl_list_store_adapter_get_path; + iface->get_value = dzl_list_store_adapter_get_value; + iface->iter_next = dzl_list_store_adapter_iter_next; + iface->iter_previous = dzl_list_store_adapter_iter_previous; + iface->iter_children = dzl_list_store_adapter_iter_children; + iface->iter_has_child = dzl_list_store_adapter_iter_has_child; + iface->iter_n_children = dzl_list_store_adapter_iter_n_children; + iface->iter_nth_child = dzl_list_store_adapter_iter_nth_child; + iface->iter_parent = dzl_list_store_adapter_iter_parent; +} + +static void +dzl_list_store_adapter_items_changed (DzlListStoreAdapter *self, + guint position, + guint removed, + guint added, + GListModel *model) +{ + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + GtkTreePath *path; + GtkTreeIter iter = { 0 }; + + g_assert (DZL_IS_LIST_STORE_ADAPTER (self)); + g_assert (G_IS_LIST_MODEL (model)); + + priv->length -= removed; + priv->length += added; + + path = gtk_tree_path_new_from_indices (position, -1); + + for (guint i = 0; i < removed; i++) + gtk_tree_model_row_deleted (GTK_TREE_MODEL (self), path); + + for (guint i = 0; i < added; i++) + { + iter.user_data = GINT_TO_POINTER (position + i); + gtk_tree_model_row_inserted (GTK_TREE_MODEL (self), path, &iter); + gtk_tree_path_next (path); + } + + gtk_tree_path_free (path); +} + +static void +dzl_list_store_adapter_bind (DzlListStoreAdapter *self, + GListModel *model, + DzlSignalGroup *signals) +{ + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + + g_assert (DZL_IS_LIST_STORE_ADAPTER (self)); + g_assert (G_IS_LIST_MODEL (model)); + g_assert (DZL_IS_SIGNAL_GROUP (signals)); + + priv->model = model; + priv->type = g_list_model_get_item_type (model); + priv->length = g_list_model_get_n_items (model); +} + +static void +dzl_list_store_adapter_unbind (DzlListStoreAdapter *self, + DzlSignalGroup *signals) +{ + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + + g_assert (DZL_IS_LIST_STORE_ADAPTER (self)); + g_assert (DZL_IS_SIGNAL_GROUP (signals)); + + priv->model = NULL; + priv->length = 0; + priv->type = G_TYPE_OBJECT; +} + +static void +dzl_list_store_adapter_finalize (GObject *object) +{ + DzlListStoreAdapter *self = (DzlListStoreAdapter *)object; + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + + g_clear_object (&priv->signals); + + G_OBJECT_CLASS (dzl_list_store_adapter_parent_class)->finalize (object); +} + +static void +dzl_list_store_adapter_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlListStoreAdapter *self = DZL_LIST_STORE_ADAPTER (object); + + switch (prop_id) + { + case PROP_MODEL: + g_value_set_object (value, dzl_list_store_adapter_get_model (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_list_store_adapter_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlListStoreAdapter *self = DZL_LIST_STORE_ADAPTER (object); + + switch (prop_id) + { + case PROP_MODEL: + dzl_list_store_adapter_set_model (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_list_store_adapter_class_init (DzlListStoreAdapterClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_list_store_adapter_finalize; + object_class->get_property = dzl_list_store_adapter_get_property; + object_class->set_property = dzl_list_store_adapter_set_property; + + properties [PROP_MODEL] = + g_param_spec_object ("model", + "Model", + "The model to be adapted", + G_TYPE_LIST_MODEL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_list_store_adapter_init (DzlListStoreAdapter *self) +{ + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + + priv->type = G_TYPE_OBJECT; + priv->signals = dzl_signal_group_new (G_TYPE_LIST_MODEL); + + dzl_signal_group_connect_swapped (priv->signals, + "items-changed", + G_CALLBACK (dzl_list_store_adapter_items_changed), + self); + + g_signal_connect_swapped (priv->signals, + "bind", + G_CALLBACK (dzl_list_store_adapter_bind), + self); + + g_signal_connect_swapped (priv->signals, + "unbind", + G_CALLBACK (dzl_list_store_adapter_unbind), + self); +} + +DzlListStoreAdapter * +dzl_list_store_adapter_new (GListModel *model) +{ + return g_object_new (DZL_TYPE_LIST_STORE_ADAPTER, + "model", model, + NULL); +} + +/** + * dzl_list_store_adapter_get_model: + * @self: A #DzlListStoreAdapter + * + * Gets the model being adapted. + * + * Returns: (transfer none): A #GListModel + * + * Since: 3.26 + */ +GListModel * +dzl_list_store_adapter_get_model (DzlListStoreAdapter *self) +{ + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_LIST_STORE_ADAPTER (self), NULL); + + return dzl_signal_group_get_target (priv->signals); +} + +void +dzl_list_store_adapter_set_model (DzlListStoreAdapter *self, + GListModel *model) +{ + DzlListStoreAdapterPrivate *priv = dzl_list_store_adapter_get_instance_private (self); + + g_return_if_fail (DZL_IS_LIST_STORE_ADAPTER (self)); + g_return_if_fail (!model || G_IS_LIST_MODEL (model)); + + dzl_signal_group_set_target (priv->signals, model); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]); +} diff --git a/src/tree/dzl-list-store-adapter.h b/src/tree/dzl-list-store-adapter.h new file mode 100644 index 0000000..cac291b --- /dev/null +++ b/src/tree/dzl-list-store-adapter.h @@ -0,0 +1,48 @@ +/* dzl-list-store-adapter.h + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#ifndef DZL_LIST_STORE_ADAPTER_H +#define DZL_LIST_STORE_ADAPTER_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_LIST_STORE_ADAPTER (dzl_list_store_adapter_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlListStoreAdapter, dzl_list_store_adapter, DZL, LIST_STORE_ADAPTER, GObject) + +struct _DzlListStoreAdapterClass +{ + GObjectClass parent_class; +}; + +DZL_AVAILABLE_IN_ALL +DzlListStoreAdapter *dzl_list_store_adapter_new (GListModel *model); +DZL_AVAILABLE_IN_ALL +GListModel *dzl_list_store_adapter_get_model (DzlListStoreAdapter *self); +DZL_AVAILABLE_IN_ALL +void dzl_list_store_adapter_set_model (DzlListStoreAdapter *self, + GListModel *model); + +G_END_DECLS + +#endif /* DZL_LIST_STORE_ADAPTER_H */ diff --git a/src/tree/dzl-tree-builder.c b/src/tree/dzl-tree-builder.c new file mode 100644 index 0000000..e14c163 --- /dev/null +++ b/src/tree/dzl-tree-builder.c @@ -0,0 +1,566 @@ +/* dzl-tree-builder.c + * + * Copyright (C) 2011-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-tree-builder" + +#include "config.h" + +#include + +#include "dzl-enums.h" + +#include "tree/dzl-tree.h" +#include "tree/dzl-tree-builder.h" +#include "util/dzl-macros.h" + +typedef struct +{ + DzlTree *tree; +} DzlTreeBuilderPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlTreeBuilder, dzl_tree_builder, G_TYPE_INITIALLY_UNOWNED) + +enum { + PROP_0, + PROP_TREE, + LAST_PROP +}; + +enum { + ADDED, + REMOVED, + BUILD_CHILDREN, + BUILD_NODE, + DRAG_DATA_GET, + DRAG_DATA_RECEIVED, + DRAG_NODE_RECEIVED, + DRAG_NODE_DELETE, + NODE_ACTIVATED, + NODE_COLLAPSED, + NODE_DRAGGABLE, + NODE_DROPPABLE, + NODE_EXPANDED, + NODE_POPUP, + NODE_SELECTED, + NODE_UNSELECTED, + LAST_SIGNAL +}; + +static GParamSpec *properties [LAST_PROP]; +static guint signals [LAST_SIGNAL]; + +gboolean +_dzl_tree_builder_node_activated (DzlTreeBuilder *builder, + DzlTreeNode *node) +{ + gboolean ret = FALSE; + + g_return_val_if_fail (DZL_IS_TREE_BUILDER(builder), FALSE); + g_return_val_if_fail (DZL_IS_TREE_NODE(node), FALSE); + + g_signal_emit (builder, signals [NODE_ACTIVATED], 0, node, &ret); + + return ret; +} + +void +_dzl_tree_builder_node_popup (DzlTreeBuilder *builder, + DzlTreeNode *node, + GMenu *menu) +{ + g_return_if_fail (DZL_IS_TREE_BUILDER (builder)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + g_return_if_fail (G_IS_MENU (menu)); + + g_signal_emit (builder, signals [NODE_POPUP], 0, node, menu); +} + +void +_dzl_tree_builder_node_selected (DzlTreeBuilder *builder, + DzlTreeNode *node) +{ + g_return_if_fail (DZL_IS_TREE_BUILDER (builder)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + g_signal_emit (builder, signals [NODE_SELECTED], 0, node); +} + +void +_dzl_tree_builder_node_unselected (DzlTreeBuilder *builder, + DzlTreeNode *node) +{ + g_return_if_fail (DZL_IS_TREE_BUILDER (builder)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + g_signal_emit (builder, signals [NODE_UNSELECTED], 0, node); +} + +void +_dzl_tree_builder_build_children (DzlTreeBuilder *builder, + DzlTreeNode *node) +{ + g_return_if_fail (DZL_IS_TREE_BUILDER (builder)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + g_signal_emit (builder, signals [BUILD_CHILDREN], 0, node); +} + +void +_dzl_tree_builder_build_node (DzlTreeBuilder *builder, + DzlTreeNode *node) +{ + g_return_if_fail (DZL_IS_TREE_BUILDER (builder)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + g_signal_emit (builder, signals [BUILD_NODE], 0, node); +} + +void +_dzl_tree_builder_added (DzlTreeBuilder *builder, + DzlTree *tree) +{ + g_return_if_fail (DZL_IS_TREE_BUILDER (builder)); + g_return_if_fail (DZL_IS_TREE (tree)); + + g_signal_emit (builder, signals [ADDED], 0, tree); +} + +void +_dzl_tree_builder_removed (DzlTreeBuilder *builder, + DzlTree *tree) +{ + g_return_if_fail (DZL_IS_TREE_BUILDER (builder)); + g_return_if_fail (DZL_IS_TREE (tree)); + + g_signal_emit (builder, signals [REMOVED], 0, tree); +} + +void +_dzl_tree_builder_node_collapsed (DzlTreeBuilder *builder, + DzlTreeNode *node) +{ + g_return_if_fail (DZL_IS_TREE_BUILDER (builder)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + g_signal_emit (builder, signals [NODE_COLLAPSED], 0, node); +} + +void +_dzl_tree_builder_node_expanded (DzlTreeBuilder *builder, + DzlTreeNode *node) +{ + g_return_if_fail (DZL_IS_TREE_BUILDER (builder)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + g_signal_emit (builder, signals [NODE_EXPANDED], 0, node); +} + +gboolean +_dzl_tree_builder_drag_node_received (DzlTreeBuilder *builder, + DzlTreeNode *drag_node, + DzlTreeNode *drop_node, + DzlTreeDropPosition position, + GdkDragAction action, + GtkSelectionData *data) +{ + gboolean ret = FALSE; + + g_return_val_if_fail (DZL_IS_TREE_BUILDER (builder), FALSE); + g_return_val_if_fail (DZL_IS_TREE_NODE (drag_node), FALSE); + g_return_val_if_fail (DZL_IS_TREE_NODE (drop_node), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + + g_signal_emit (builder, signals [DRAG_NODE_RECEIVED], 0, + drag_node, drop_node, position, action, data, + &ret); + + return ret; +} + +gboolean +_dzl_tree_builder_node_draggable (DzlTreeBuilder *builder, + DzlTreeNode *node) +{ + gboolean ret = FALSE; + + g_return_val_if_fail (DZL_IS_TREE_BUILDER (builder), FALSE); + g_return_val_if_fail (DZL_IS_TREE_NODE (node), FALSE); + + g_signal_emit (builder, signals [NODE_DRAGGABLE], 0, node, &ret); + + return ret; +} + +gboolean +_dzl_tree_builder_node_droppable (DzlTreeBuilder *builder, + DzlTreeNode *node, + GtkSelectionData *data) +{ + gboolean ret = FALSE; + + g_return_val_if_fail (DZL_IS_TREE_BUILDER (builder), FALSE); + g_return_val_if_fail (DZL_IS_TREE_NODE (node), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + + g_signal_emit (builder, signals [NODE_DROPPABLE], 0, node, data, &ret); + + return ret; +} + +gboolean +_dzl_tree_builder_drag_node_delete (DzlTreeBuilder *builder, + DzlTreeNode *node) +{ + gboolean ret = FALSE; + + g_return_val_if_fail (DZL_IS_TREE_BUILDER (builder), FALSE); + g_return_val_if_fail (DZL_IS_TREE_NODE (node), FALSE); + + g_signal_emit (builder, signals [DRAG_NODE_DELETE], 0, node, &ret); + + return ret; +} + +gboolean +_dzl_tree_builder_drag_data_get (DzlTreeBuilder *builder, + DzlTreeNode *node, + GtkSelectionData *data) +{ + gboolean ret = FALSE; + + g_return_val_if_fail (DZL_IS_TREE_BUILDER (builder), FALSE); + g_return_val_if_fail (DZL_IS_TREE_NODE (node), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + + g_signal_emit (builder, signals [DRAG_DATA_GET], 0, node, data, &ret); + + return ret; +} + +gboolean +_dzl_tree_builder_drag_data_received (DzlTreeBuilder *builder, + DzlTreeNode *drop_node, + DzlTreeDropPosition position, + GdkDragAction action, + GtkSelectionData *data) +{ + gboolean ret = FALSE; + + g_return_val_if_fail (DZL_IS_TREE_BUILDER (builder), FALSE); + g_return_val_if_fail (DZL_IS_TREE_NODE (drop_node), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + + g_signal_emit (builder, signals [DRAG_DATA_RECEIVED], 0, + drop_node, position, action, data, + &ret); + + return ret; +} + +void +_dzl_tree_builder_set_tree (DzlTreeBuilder *builder, + DzlTree *tree) +{ + DzlTreeBuilderPrivate *priv = dzl_tree_builder_get_instance_private (builder); + + g_return_if_fail (DZL_IS_TREE_BUILDER (builder)); + g_return_if_fail (priv->tree == NULL || DZL_IS_TREE (priv->tree)); + g_return_if_fail (DZL_IS_TREE (tree)); + + if (dzl_set_weak_pointer (&priv->tree, tree)) + g_object_notify_by_pspec (G_OBJECT (builder), properties [PROP_TREE]); +} + +/** + * dzl_tree_builder_get_tree: + * @builder: (in): A #DzlTreeBuilder. + * + * Gets the tree that owns the builder. + * + * Returns: (transfer none) (nullable): A #DzlTree or %NULL. + */ +DzlTree * +dzl_tree_builder_get_tree (DzlTreeBuilder *builder) +{ + DzlTreeBuilderPrivate *priv = dzl_tree_builder_get_instance_private (builder); + + g_return_val_if_fail (DZL_IS_TREE_BUILDER (builder), NULL); + + return priv->tree; +} + +static void +dzl_tree_builder_finalize (GObject *object) +{ + DzlTreeBuilder *builder = DZL_TREE_BUILDER (object); + DzlTreeBuilderPrivate *priv = dzl_tree_builder_get_instance_private (builder); + + dzl_clear_weak_pointer (&priv->tree); + + G_OBJECT_CLASS (dzl_tree_builder_parent_class)->finalize (object); +} + +static void +dzl_tree_builder_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlTreeBuilder *builder = DZL_TREE_BUILDER (object); + DzlTreeBuilderPrivate *priv = dzl_tree_builder_get_instance_private (builder); + + switch (prop_id) + { + case PROP_TREE: + g_value_set_object (value, priv->tree); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_tree_builder_class_init (DzlTreeBuilderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_tree_builder_finalize; + object_class->get_property = dzl_tree_builder_get_property; + + properties[PROP_TREE] = + g_param_spec_object("tree", + "Tree", + "The DzlTree the builder belongs to.", + DZL_TYPE_TREE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + signals [ADDED] = + g_signal_new ("added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, added), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + DZL_TYPE_TREE); + g_signal_set_va_marshaller (signals [ADDED], + G_TYPE_FROM_CLASS (klass), + g_cclosure_marshal_VOID__OBJECTv); + + signals [BUILD_NODE] = + g_signal_new ("build-node", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, build_node), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + DZL_TYPE_TREE_NODE); + g_signal_set_va_marshaller (signals [BUILD_NODE], + G_TYPE_FROM_CLASS (klass), + g_cclosure_marshal_VOID__OBJECTv); + + signals [BUILD_CHILDREN] = + g_signal_new ("build-children", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, build_children), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + DZL_TYPE_TREE_NODE); + g_signal_set_va_marshaller (signals [BUILD_CHILDREN], + G_TYPE_FROM_CLASS (klass), + g_cclosure_marshal_VOID__OBJECTv); + + signals [DRAG_NODE_RECEIVED] = + g_signal_new ("drag-node-received", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, drag_node_received), + NULL, NULL, NULL, + G_TYPE_BOOLEAN, + 5, + DZL_TYPE_TREE_NODE, + DZL_TYPE_TREE_NODE, + DZL_TYPE_TREE_DROP_POSITION, + GDK_TYPE_DRAG_ACTION, + GTK_TYPE_SELECTION_DATA); + + signals [DRAG_NODE_DELETE] = + g_signal_new ("drag-node-delete", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, drag_node_delete), + NULL, NULL, NULL, + G_TYPE_BOOLEAN, 1, DZL_TYPE_TREE_NODE); + + signals [NODE_ACTIVATED] = + g_signal_new ("node-activated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, node_activated), + NULL, NULL, NULL, + G_TYPE_BOOLEAN, + 1, + DZL_TYPE_TREE_NODE); + + signals [NODE_DRAGGABLE] = + g_signal_new ("node-draggable", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, node_draggable), + g_signal_accumulator_true_handled, NULL, + NULL, + G_TYPE_BOOLEAN, + 1, DZL_TYPE_TREE_NODE); + + signals [DRAG_DATA_GET] = + g_signal_new ("drag-data-get", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, drag_data_get), + g_signal_accumulator_true_handled, NULL, + NULL, + G_TYPE_BOOLEAN, + 2, + DZL_TYPE_TREE_NODE, + GTK_TYPE_SELECTION_DATA); + + signals [DRAG_DATA_RECEIVED] = + g_signal_new ("drag-data-received", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, drag_data_received), + g_signal_accumulator_true_handled, NULL, + NULL, + G_TYPE_BOOLEAN, + 4, + DZL_TYPE_TREE_NODE, + DZL_TYPE_TREE_DROP_POSITION, + GDK_TYPE_DRAG_ACTION, + GTK_TYPE_SELECTION_DATA); + + signals [NODE_DROPPABLE] = + g_signal_new ("node-droppable", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, node_droppable), + g_signal_accumulator_true_handled, NULL, + NULL, + G_TYPE_BOOLEAN, + 2, DZL_TYPE_TREE_NODE, GTK_TYPE_SELECTION_DATA); + + signals [NODE_COLLAPSED] = + g_signal_new ("node-collapsed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, node_collapsed), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + DZL_TYPE_TREE_NODE); + g_signal_set_va_marshaller (signals [NODE_COLLAPSED], + G_TYPE_FROM_CLASS (klass), + g_cclosure_marshal_VOID__OBJECTv); + + signals [NODE_EXPANDED] = + g_signal_new ("node-expanded", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, node_expanded), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + DZL_TYPE_TREE_NODE); + g_signal_set_va_marshaller (signals [NODE_EXPANDED], + G_TYPE_FROM_CLASS (klass), + g_cclosure_marshal_VOID__OBJECTv); + + signals [NODE_POPUP] = + g_signal_new ("node-popup", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, node_popup), + NULL, NULL, NULL, + G_TYPE_NONE, + 2, + DZL_TYPE_TREE_NODE, + G_TYPE_MENU); + + signals [NODE_SELECTED] = + g_signal_new ("node-selected", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, node_selected), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + DZL_TYPE_TREE_NODE); + g_signal_set_va_marshaller (signals [NODE_SELECTED], + G_TYPE_FROM_CLASS (klass), + g_cclosure_marshal_VOID__OBJECTv); + + signals [NODE_UNSELECTED] = + g_signal_new ("node-unselected", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, node_unselected), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + DZL_TYPE_TREE_NODE); + g_signal_set_va_marshaller (signals [NODE_UNSELECTED], + G_TYPE_FROM_CLASS (klass), + g_cclosure_marshal_VOID__OBJECTv); + + signals [REMOVED] = + g_signal_new ("removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeBuilderClass, removed), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + DZL_TYPE_TREE); + g_signal_set_va_marshaller (signals [REMOVED], + G_TYPE_FROM_CLASS (klass), + g_cclosure_marshal_VOID__OBJECTv); +} + +static void +dzl_tree_builder_init (DzlTreeBuilder *builder) +{ +} + +DzlTreeBuilder * +dzl_tree_builder_new (void) +{ + return g_object_new (DZL_TYPE_TREE_BUILDER, NULL); +} diff --git a/src/tree/dzl-tree-builder.h b/src/tree/dzl-tree-builder.h new file mode 100644 index 0000000..0fb8c22 --- /dev/null +++ b/src/tree/dzl-tree-builder.h @@ -0,0 +1,93 @@ +/* dzl-tree-builder.h + * + * Copyright (C) 2011-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_TREE_BUILDER_H +#define DZL_TREE_BUILDER_H + +#include + +#include "dzl-version-macros.h" + +#include "tree/dzl-tree-node.h" +#include "tree/dzl-tree-types.h" + +G_BEGIN_DECLS + +struct _DzlTreeBuilderClass +{ + GInitiallyUnownedClass parent_class; + + void (*added) (DzlTreeBuilder *builder, + GtkWidget *tree); + void (*removed) (DzlTreeBuilder *builder, + GtkWidget *tree); + void (*build_node) (DzlTreeBuilder *builder, + DzlTreeNode *node); + void (*build_children) (DzlTreeBuilder *builder, + DzlTreeNode *parent); + gboolean (*node_activated) (DzlTreeBuilder *builder, + DzlTreeNode *node); + void (*node_selected) (DzlTreeBuilder *builder, + DzlTreeNode *node); + void (*node_unselected) (DzlTreeBuilder *builder, + DzlTreeNode *node); + void (*node_popup) (DzlTreeBuilder *builder, + DzlTreeNode *node, + GMenu *menu); + void (*node_expanded) (DzlTreeBuilder *builder, + DzlTreeNode *node); + void (*node_collapsed) (DzlTreeBuilder *builder, + DzlTreeNode *node); + gboolean (*node_draggable) (DzlTreeBuilder *builder, + DzlTreeNode *node); + gboolean (*node_droppable) (DzlTreeBuilder *builder, + DzlTreeNode *node, + GtkSelectionData *data); + gboolean (*drag_data_get) (DzlTreeBuilder *builder, + DzlTreeNode *node, + GtkSelectionData *data); + gboolean (*drag_node_received) (DzlTreeBuilder *builder, + DzlTreeNode *drag_node, + DzlTreeNode *drop_node, + DzlTreeDropPosition position, + GdkDragAction action, + GtkSelectionData *data); + gboolean (*drag_data_received) (DzlTreeBuilder *builder, + DzlTreeNode *drop_node, + DzlTreeDropPosition position, + GdkDragAction action, + GtkSelectionData *data); + gboolean (*drag_node_delete) (DzlTreeBuilder *builder, + DzlTreeNode *node); + void (*cell_data_func) (DzlTreeBuilder *tree, + DzlTreeNode *node, + GtkCellRenderer *cell); + + /*< private >*/ + gpointer _padding[11]; +}; + +DZL_AVAILABLE_IN_ALL +DzlTree *dzl_tree_builder_get_tree (DzlTreeBuilder *builder); + +DZL_AVAILABLE_IN_3_28 +DzlTreeBuilder *dzl_tree_builder_new (void); + +G_END_DECLS + +#endif /* DZL_TREE_BUILDER_H */ diff --git a/src/tree/dzl-tree-node.c b/src/tree/dzl-tree-node.c new file mode 100644 index 0000000..55ea711 --- /dev/null +++ b/src/tree/dzl-tree-node.c @@ -0,0 +1,1415 @@ +/* dzl-tree-node.c + * + * Copyright (C) 2011-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-tree-node" + +#include "config.h" + +#include +#include + +#include "tree/dzl-tree.h" +#include "tree/dzl-tree-node.h" +#include "tree/dzl-tree-private.h" +#include "util/dzl-macros.h" + +struct _DzlTreeNode +{ + GInitiallyUnowned parent_instance; + + DzlTreeNode *parent; + + GObject *item; + gchar *text; + DzlTree *tree; + + GIcon *gicon; + GList *emblems; + GQuark icon_name; + GQuark expanded_icon_name; + + GdkRGBA foreground_rgba; + + guint children_possible : 1; + guint is_dummy : 1; + guint foreground_rgba_set : 1; + guint needs_build_children : 1; + guint reset_on_collapse : 1; + guint use_dim_label : 1; + guint use_markup : 1; +}; + +typedef struct +{ + DzlTreeNode *self; + GtkPopover *popover; +} PopupRequest; + +G_DEFINE_TYPE (DzlTreeNode, dzl_tree_node, G_TYPE_INITIALLY_UNOWNED) + +enum { + PROP_0, + PROP_CHILDREN_POSSIBLE, + PROP_EXPANDED_ICON_NAME, + PROP_ICON_NAME, + PROP_GICON, + PROP_ITEM, + PROP_PARENT, + PROP_RESET_ON_COLLAPSE, + PROP_TEXT, + PROP_TREE, + PROP_USE_DIM_LABEL, + PROP_USE_MARKUP, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +/** + * dzl_tree_node_new: + * + * Creates a new #DzlTreeNode instance. This is handy for situations where you + * do not want to subclass #DzlTreeNode. + * + * Returns: (transfer full): A #DzlTreeNode + */ +DzlTreeNode * +dzl_tree_node_new (void) +{ + return g_object_new (DZL_TYPE_TREE_NODE, NULL); +} + +/** + * dzl_tree_node_get_tree: + * @node: (in): A #DzlTreeNode. + * + * Fetches the #DzlTree instance that owns the node. + * + * Returns: (transfer none): A #DzlTree. + */ +DzlTree * +dzl_tree_node_get_tree (DzlTreeNode *node) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (node), NULL); + + while (node != NULL) + { + if (node->tree != NULL) + return node->tree; + node = node->parent; + } + + return NULL; +} + +/** + * dzl_tree_node_set_tree: + * @node: (in): A #DzlTreeNode. + * @tree: (in): A #DzlTree. + * + * Internal method to set the nodes tree. + */ +void +_dzl_tree_node_set_tree (DzlTreeNode *node, + DzlTree *tree) +{ + g_return_if_fail (DZL_IS_TREE_NODE (node)); + g_return_if_fail (!tree || DZL_IS_TREE (tree)); + + dzl_set_weak_pointer (&node->tree, tree); +} + +/** + * dzl_tree_node_insert: + * @self: a #DzlTreeNode + * @child: a #DzlTreeNode + * @position: the position for the child + * + * Inserts @child as a child of @self at @position. + * + * Since: 3.28 + */ +void +dzl_tree_node_insert (DzlTreeNode *self, + DzlTreeNode *child, + guint position) +{ + g_return_if_fail (DZL_IS_TREE_NODE (self)); + g_return_if_fail (DZL_IS_TREE_NODE (child)); + + _dzl_tree_insert (self->tree, self, child, position); +} + +/** + * dzl_tree_node_insert_sorted: + * @node: A #DzlTreeNode. + * @child: A #DzlTreeNode. + * @compare_func: (scope call): A compare func to compare nodes. + * @user_data: user data for @compare_func. + * + * Inserts a @child as a child of @node, sorting it among the other children. + */ +void +dzl_tree_node_insert_sorted (DzlTreeNode *node, + DzlTreeNode *child, + DzlTreeNodeCompareFunc compare_func, + gpointer user_data) +{ + g_return_if_fail (DZL_IS_TREE_NODE (node)); + g_return_if_fail (DZL_IS_TREE_NODE (child)); + g_return_if_fail (compare_func != NULL); + + _dzl_tree_insert_sorted (node->tree, node, child, compare_func, user_data); +} + +/** + * dzl_tree_node_append: + * @node: A #DzlTreeNode. + * @child: A #DzlTreeNode. + * + * Appends @child to the list of children owned by @node. + */ +void +dzl_tree_node_append (DzlTreeNode *node, + DzlTreeNode *child) +{ + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + _dzl_tree_append (node->tree, node, child); +} + +/** + * dzl_tree_node_prepend: + * @node: A #DzlTreeNode. + * @child: A #DzlTreeNode. + * + * Prepends @child to the list of children owned by @node. + */ +void +dzl_tree_node_prepend (DzlTreeNode *node, + DzlTreeNode *child) +{ + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + _dzl_tree_prepend (node->tree, node, child); +} + +/** + * dzl_tree_node_remove: + * @node: A #DzlTreeNode. + * @child: A #DzlTreeNode. + * + * Removes @child from the list of children owned by @node. + */ +void +dzl_tree_node_remove (DzlTreeNode *node, + DzlTreeNode *child) +{ + g_return_if_fail (DZL_IS_TREE_NODE (node)); + g_return_if_fail (DZL_IS_TREE_NODE (child)); + + _dzl_tree_remove (node->tree, child); +} + +/** + * dzl_tree_node_get_path: + * @node: (in): A #DzlTreeNode. + * + * Gets a #GtkTreePath for @node. + * + * Returns: (nullable) (transfer full): A #GtkTreePath if successful; otherwise %NULL. + */ +GtkTreePath * +dzl_tree_node_get_path (DzlTreeNode *node) +{ + DzlTreeNode *toplevel; + GtkTreePath *path; + GList *list = NULL; + + g_return_val_if_fail (DZL_IS_TREE_NODE (node), NULL); + + if ((node->parent == NULL) || (node->tree == NULL)) + return NULL; + + do + { + list = g_list_prepend (list, node); + } + while ((node = node->parent)); + + toplevel = list->data; + + g_assert (toplevel); + g_assert (toplevel->tree); + + path = _dzl_tree_get_path (toplevel->tree, list); + + g_list_free (list); + + return path; +} + +gboolean +dzl_tree_node_get_iter (DzlTreeNode *self, + GtkTreeIter *iter) +{ + gboolean ret = FALSE; + + g_return_val_if_fail (DZL_IS_TREE_NODE (self), FALSE); + g_return_val_if_fail (iter != NULL, FALSE); + + if (self->tree != NULL) + ret = _dzl_tree_get_iter (self->tree, self, iter); + + return ret; +} + +/** + * dzl_tree_node_get_parent: + * @node: (in): A #DzlTreeNode. + * + * Retrieves the parent #DzlTreeNode for @node. + * + * Returns: (transfer none): A #DzlTreeNode. + */ +DzlTreeNode * +dzl_tree_node_get_parent (DzlTreeNode *node) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (node), NULL); + + return node->parent; +} + +static void +dzl_tree_node_queue_draw (DzlTreeNode *self) +{ + g_assert (DZL_IS_TREE_NODE (self)); + + if (self->tree != NULL) + gtk_widget_queue_draw (GTK_WIDGET (self->tree)); +} + +void +dzl_tree_node_set_gicon (DzlTreeNode *self, + GIcon *gicon) +{ + g_return_if_fail (DZL_IS_TREE_NODE (self)); + g_return_if_fail (!gicon || G_IS_ICON (gicon)); + + if (g_set_object (&self->gicon, gicon)) + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_GICON]); +} + +/** + * dzl_tree_node_get_gicon: + * + * Fetch the GIcon, re-render if necessary + * + * Returns: (transfer none): An #GIcon or %NULL. + */ +GIcon * +dzl_tree_node_get_gicon (DzlTreeNode *self) +{ + const gchar *icon_name; + + g_return_val_if_fail (DZL_IS_TREE_NODE (self), NULL); + + icon_name = dzl_tree_node_get_icon_name (self); + + if G_UNLIKELY (self->gicon == NULL && icon_name != NULL) + { + g_autoptr(GIcon) base = NULL; + g_autoptr(GIcon) icon = NULL; + + base = g_themed_icon_new (icon_name); + icon = g_emblemed_icon_new (base, NULL); + + for (GList *iter = self->emblems; iter != NULL; iter = iter->next) + { + const gchar *emblem_icon_name = iter->data; + g_autoptr(GIcon) emblem_base = NULL; + g_autoptr(GEmblem) emblem = NULL; + + emblem_base = g_themed_icon_new (emblem_icon_name); + emblem = g_emblem_new (emblem_base); + + g_emblemed_icon_add_emblem (G_EMBLEMED_ICON (icon), emblem); + } + + if (g_set_object (&self->gicon, icon)) + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_GICON]); + } + + return self->gicon; +} + +/** + * dzl_tree_node_get_icon_name: + * + * Fetches the icon-name of the icon to display, or NULL for no icon. + */ +const gchar * +dzl_tree_node_get_icon_name (DzlTreeNode *node) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (node), NULL); + + return g_quark_to_string (node->icon_name); +} + +/** + * dzl_tree_node_set_icon_name: + * @node: A #DzlTreeNode. + * @icon_name: (nullable): The icon name. + * + * Sets the icon name of the node. This is displayed in the pixbuf + * cell of the DzlTree. + */ +void +dzl_tree_node_set_icon_name (DzlTreeNode *node, + const gchar *icon_name) +{ + GQuark value = 0; + + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + if (icon_name != NULL) + value = g_quark_from_string (icon_name); + + if (value != node->icon_name) + { + node->icon_name = value; + g_clear_object (&node->gicon); + g_object_notify_by_pspec (G_OBJECT (node), properties [PROP_ICON_NAME]); + g_object_notify_by_pspec (G_OBJECT (node), properties [PROP_GICON]); + dzl_tree_node_queue_draw (node); + } +} + +/** + * dzl_tree_node_add_emblem: + * @self: An #DzlTreeNode + * @emblem_name: the icon-name of the emblem + * + * Adds an emplem to be rendered on top of the node. + * + * Use dzl_tree_node_remove_emblem() to remove an emblem. + */ +void +dzl_tree_node_add_emblem (DzlTreeNode *self, + const gchar *emblem) +{ + g_return_if_fail (DZL_IS_TREE_NODE (self)); + + for (GList *iter = self->emblems; iter != NULL; iter = iter->next) + { + const gchar *iter_icon_name = iter->data; + + if (g_strcmp0 (iter_icon_name, emblem) == 0) + return; + } + + self->emblems = g_list_prepend (self->emblems, g_strdup (emblem)); + g_clear_object (&self->gicon); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_GICON]); + dzl_tree_node_queue_draw (self); +} + +void +dzl_tree_node_remove_emblem (DzlTreeNode *self, + const gchar *emblem_name) +{ + g_return_if_fail (DZL_IS_TREE_NODE (self)); + + for (GList *iter = self->emblems; iter != NULL; iter = iter->next) + { + gchar *iter_icon_name = iter->data; + + if (g_strcmp0 (iter_icon_name, emblem_name) == 0) + { + g_free (iter_icon_name); + self->emblems = g_list_delete_link (self->emblems, iter); + g_clear_object (&self->gicon); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_GICON]); + dzl_tree_node_queue_draw (self); + return; + } + } +} + +/** + * dzl_tree_node_clear_emblems: + * + * Removes all emblems from @self. + */ +void +dzl_tree_node_clear_emblems (DzlTreeNode *self) +{ + g_return_if_fail (DZL_IS_TREE_NODE (self)); + + g_list_free_full (self->emblems, g_free); + self->emblems = NULL; + g_clear_object (&self->gicon); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_GICON]); + dzl_tree_node_queue_draw (self); +} + +/** + * dzl_tree_node_has_emblem: + * @self: An #DzlTreeNode + * @emblem_name: a string containing the emblem name + * + * Checks to see if @emblem_name has been added to the #DzlTreeNode. + * + * Returns: %TRUE if @emblem_name is used by @self + */ +gboolean +dzl_tree_node_has_emblem (DzlTreeNode *self, + const gchar *emblem_name) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (self), FALSE); + + for (GList *iter = self->emblems; iter != NULL; iter = iter->next) + { + const gchar *iter_icon_name = iter->data; + + if (g_strcmp0 (iter_icon_name, emblem_name) == 0) + return TRUE; + } + + return FALSE; +} + +void +dzl_tree_node_set_emblems (DzlTreeNode *self, + const gchar * const *emblems) +{ + g_return_if_fail (DZL_IS_TREE_NODE (self)); + + if (self->emblems != NULL) + { + g_list_free_full (self->emblems, g_free); + self->emblems = NULL; + } + + if (emblems != NULL) + { + guint len = g_strv_length ((gchar **)emblems); + + for (guint i = len; i > 0; i--) + self->emblems = g_list_prepend (self->emblems, g_strdup (emblems[i-1])); + } + + g_clear_object (&self->gicon); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_GICON]); + dzl_tree_node_queue_draw (self); +} + +/** + * dzl_tree_node_set_item: + * @node: (in): A #DzlTreeNode. + * @item: (in): A #GObject. + * + * An optional object to associate with the node. This is handy to save needing + * to subclass the #DzlTreeNode class. + */ +void +dzl_tree_node_set_item (DzlTreeNode *node, + GObject *item) +{ + g_return_if_fail (DZL_IS_TREE_NODE (node)); + g_return_if_fail (!item || G_IS_OBJECT (item)); + + if (g_set_object (&node->item, item)) + g_object_notify_by_pspec (G_OBJECT (node), properties [PROP_ITEM]); +} + +void +_dzl_tree_node_set_parent (DzlTreeNode *node, + DzlTreeNode *parent) +{ + g_return_if_fail (DZL_IS_TREE_NODE (node)); + g_return_if_fail (node->parent == NULL); + g_return_if_fail (!parent || DZL_IS_TREE_NODE (parent)); + + dzl_set_weak_pointer (&node->parent, parent); +} + +const gchar * +dzl_tree_node_get_text (DzlTreeNode *node) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (node), NULL); + + return node->text; +} + +/** + * dzl_tree_node_set_text: + * @node: A #DzlTreeNode. + * @text: (nullable): The node text. + * + * Sets the text of the node. This is displayed in the text + * cell of the DzlTree. + */ +void +dzl_tree_node_set_text (DzlTreeNode *node, + const gchar *text) +{ + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + if (g_strcmp0 (text, node->text) != 0) + { + g_free (node->text); + node->text = g_strdup (text); + g_object_notify_by_pspec (G_OBJECT (node), properties [PROP_TEXT]); + } +} + +gboolean +dzl_tree_node_get_use_markup (DzlTreeNode *self) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (self), FALSE); + + return self->use_markup; +} + +void +dzl_tree_node_set_use_markup (DzlTreeNode *self, + gboolean use_markup) +{ + g_return_if_fail (DZL_IS_TREE_NODE (self)); + + use_markup = !!use_markup; + + if (self->use_markup != use_markup) + { + self->use_markup = use_markup; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_USE_MARKUP]); + } +} + +/** + * dzl_tree_node_get_item: + * @node: (in): A #DzlTreeNode. + * + * Gets a #GObject for the node, if one was set. + * + * Returns: (transfer none): A #GObject or %NULL. + */ +GObject * +dzl_tree_node_get_item (DzlTreeNode *node) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (node), NULL); + + return node->item; +} + +gboolean +dzl_tree_node_expand (DzlTreeNode *node, + gboolean expand_ancestors) +{ + DzlTree *tree; + GtkTreePath *path; + gboolean ret; + + g_return_val_if_fail (DZL_IS_TREE_NODE (node), FALSE); + + tree = dzl_tree_node_get_tree (node); + path = dzl_tree_node_get_path (node); + ret = gtk_tree_view_expand_row (GTK_TREE_VIEW (tree), path, FALSE); + if (expand_ancestors) + gtk_tree_view_expand_to_path (GTK_TREE_VIEW (tree), path); + gtk_tree_path_free (path); + + return ret; +} + +void +dzl_tree_node_collapse (DzlTreeNode *node) +{ + DzlTree *tree; + GtkTreePath *path; + + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + tree = dzl_tree_node_get_tree (node); + path = dzl_tree_node_get_path (node); + gtk_tree_view_collapse_row (GTK_TREE_VIEW (tree), path); + gtk_tree_path_free (path); +} + +void +dzl_tree_node_select (DzlTreeNode *node) +{ + DzlTree *tree; + GtkTreePath *path; + GtkTreeSelection *selection; + + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + tree = dzl_tree_node_get_tree (node); + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree)); + path = dzl_tree_node_get_path (node); + gtk_tree_selection_select_path (selection, path); + gtk_tree_path_free (path); +} + +void +dzl_tree_node_get_area (DzlTreeNode *node, + GdkRectangle *area) +{ + DzlTree *tree; + GtkTreeViewColumn *column; + GtkTreePath *path; + + g_return_if_fail (DZL_IS_TREE_NODE (node)); + g_return_if_fail (area != NULL); + + tree = dzl_tree_node_get_tree (node); + path = dzl_tree_node_get_path (node); + column = gtk_tree_view_get_column (GTK_TREE_VIEW (tree), 0); + gtk_tree_view_get_cell_area (GTK_TREE_VIEW (tree), path, column, area); + gtk_tree_path_free (path); +} + +void +dzl_tree_node_invalidate (DzlTreeNode *self) +{ + g_return_if_fail (DZL_IS_TREE_NODE (self)); + + if (self->tree != NULL) + _dzl_tree_invalidate (self->tree, self); +} + +gboolean +dzl_tree_node_get_expanded (DzlTreeNode *self) +{ + gboolean ret = TRUE; + + g_return_val_if_fail (DZL_IS_TREE_NODE (self), FALSE); + + if ((self->tree != NULL) && (self->parent != NULL)) + { + GtkTreePath *path = dzl_tree_node_get_path (self); + + if (path != NULL) + { + ret = gtk_tree_view_row_expanded (GTK_TREE_VIEW (self->tree), path); + gtk_tree_path_free (path); + } + } + + return ret; +} + +static void +dzl_tree_node_finalize (GObject *object) +{ + DzlTreeNode *self = DZL_TREE_NODE (object); + + g_clear_object (&self->item); + g_clear_object (&self->gicon); + g_clear_pointer (&self->text, g_free); + + g_list_free_full (self->emblems, g_free); + self->emblems = NULL; + + dzl_clear_weak_pointer (&self->tree); + dzl_clear_weak_pointer (&self->parent); + + G_OBJECT_CLASS (dzl_tree_node_parent_class)->finalize (object); +} + +static void +dzl_tree_node_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlTreeNode *node = DZL_TREE_NODE (object); + + switch (prop_id) + { + case PROP_CHILDREN_POSSIBLE: + g_value_set_boolean (value, dzl_tree_node_get_children_possible (node)); + break; + + case PROP_EXPANDED_ICON_NAME: + g_value_set_string (value, _dzl_tree_node_get_expanded_icon (node)); + break; + + case PROP_ICON_NAME: + g_value_set_string (value, g_quark_to_string (node->icon_name)); + break; + + case PROP_ITEM: + g_value_set_object (value, node->item); + break; + + case PROP_GICON: + g_value_set_object (value, node->gicon); + break; + + case PROP_PARENT: + g_value_set_object (value, node->parent); + break; + + case PROP_RESET_ON_COLLAPSE: + g_value_set_boolean (value, node->reset_on_collapse); + break; + + case PROP_TEXT: + g_value_set_string (value, node->text); + break; + + case PROP_TREE: + g_value_set_object (value, dzl_tree_node_get_tree (node)); + break; + + case PROP_USE_DIM_LABEL: + g_value_set_boolean (value, node->use_dim_label); + break; + + case PROP_USE_MARKUP: + g_value_set_boolean (value, node->use_markup); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_tree_node_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlTreeNode *node = DZL_TREE_NODE (object); + + switch (prop_id) + { + case PROP_CHILDREN_POSSIBLE: + dzl_tree_node_set_children_possible (node, g_value_get_boolean (value)); + break; + + case PROP_EXPANDED_ICON_NAME: + node->expanded_icon_name = g_quark_from_string (g_value_get_string (value)); + break; + + case PROP_GICON: + dzl_tree_node_set_gicon (node, g_value_get_object (value)); + break; + + case PROP_ICON_NAME: + dzl_tree_node_set_icon_name (node, g_value_get_string (value)); + break; + + case PROP_ITEM: + dzl_tree_node_set_item (node, g_value_get_object (value)); + break; + + case PROP_RESET_ON_COLLAPSE: + dzl_tree_node_set_reset_on_collapse (node, g_value_get_boolean (value)); + break; + + case PROP_TEXT: + dzl_tree_node_set_text (node, g_value_get_string (value)); + break; + + case PROP_USE_DIM_LABEL: + dzl_tree_node_set_use_dim_label (node, g_value_get_boolean (value)); + break; + + case PROP_USE_MARKUP: + dzl_tree_node_set_use_markup (node, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_tree_node_class_init (DzlTreeNodeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_tree_node_finalize; + object_class->get_property = dzl_tree_node_get_property; + object_class->set_property = dzl_tree_node_set_property; + + /** + * DzlTreeNode:children-possible: + * + * This property allows for more lazy loading of nodes. + * + * When a node becomes visible, we normally build its children nodes + * so that we know if we need an expansion arrow. However, that can + * be expensive when rendering directories with lots of subdirectories. + * + * Using this, you can always show an arrow without building the children + * and simply hide the arrow if there were in fact no children (upon + * expansion). + */ + properties [PROP_CHILDREN_POSSIBLE] = + g_param_spec_boolean ("children-possible", + "Children Possible", + "Allows for lazy creation of children nodes.", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_EXPANDED_ICON_NAME] = + g_param_spec_string ("expanded-icon-name", + "Expanded Icon Name", + "The icon-name to use when the row is expanded", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * DzlTreeNode:icon-name: + * + * An icon-name to display on the row. + */ + properties[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "Icon Name", + "The icon name to display.", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * DzlTreeNode:gicon: + * + * The cached GIcon to display. + */ + properties[PROP_GICON] = + g_param_spec_object ("gicon", + "GIcon", + "The GIcon object", + G_TYPE_ICON, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * DzlTreeNode:item: + * + * An optional #GObject to associate with the node. + */ + properties[PROP_ITEM] = + g_param_spec_object ("item", + "Item", + "Optional object to associate with node.", + G_TYPE_OBJECT, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * DzlTreeNode:parent: + * + * The parent of the node. + */ + properties [PROP_PARENT] = + g_param_spec_object ("parent", + "Parent", + "The parent node.", + DZL_TYPE_TREE_NODE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * DzlTreeNode:reset-on-collapse: + * + * The "reset-on-collapse" property denotes that all children should be + * removed from the node when it's row is collapsed. It will also set + * #DzlTreeNode:needs-build to %TRUE so the next expansion rebuilds the + * children. This is useful for situations where you want to ensure the nodes + * are up to date (refreshed) on every expansion. + * + * Since: 3.28 + */ + properties [PROP_RESET_ON_COLLAPSE] = + g_param_spec_boolean ("reset-on-collapse", + "Reset on Collapse", + "Reset by clearing children on collapse, requiring a rebuild on next expand", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * DzlTreeNode:tree: + * + * The tree the node belongs to. + */ + properties [PROP_TREE] = + g_param_spec_object ("tree", + "Tree", + "The DzlTree the node belongs to.", + DZL_TYPE_TREE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * DzlTreeNode:text: + * + * Text to display on the tree node. + */ + properties [PROP_TEXT] = + g_param_spec_string ("text", + "Text", + "The text of the node.", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * DzlTreeNode:use-markup: + * + * If the "text" property includes #GMarkup. + */ + properties [PROP_USE_MARKUP] = + g_param_spec_boolean ("use-markup", + "Use Markup", + "If text should be translated as markup.", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + properties [PROP_USE_DIM_LABEL] = + g_param_spec_boolean ("use-dim-label", + "Use Dim Label", + "If text should be rendered with a dim label.", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_tree_node_init (DzlTreeNode *node) +{ + node->needs_build_children = TRUE; +} + +static gboolean +dzl_tree_node_show_popover_timeout_cb (gpointer data) +{ + PopupRequest *popreq = data; + GdkRectangle rect; + GtkAllocation alloc; + DzlTree *tree; + + g_assert (popreq); + g_assert (DZL_IS_TREE_NODE (popreq->self)); + g_assert (GTK_IS_POPOVER (popreq->popover)); + + if (!(tree = dzl_tree_node_get_tree (popreq->self))) + goto cleanup; + + dzl_tree_node_get_area (popreq->self, &rect); + gtk_widget_get_allocation (GTK_WIDGET (tree), &alloc); + + if ((rect.x + rect.width) > (alloc.x + alloc.width)) + rect.width = (alloc.x + alloc.width) - rect.x; + + /* + * FIXME: Wouldn't this be better placed in a theme? + */ + switch (gtk_popover_get_position (popreq->popover)) + { + case GTK_POS_BOTTOM: + case GTK_POS_TOP: + rect.y += 3; + rect.height -= 6; + break; + case GTK_POS_RIGHT: + case GTK_POS_LEFT: + rect.x += 3; + rect.width -= 6; + break; + + default: + break; + } + + gtk_popover_set_relative_to (popreq->popover, GTK_WIDGET (tree)); + gtk_popover_set_pointing_to (popreq->popover, &rect); + gtk_popover_popup (popreq->popover); + +cleanup: + g_clear_object (&popreq->self); + g_clear_object (&popreq->popover); + g_slice_free (PopupRequest, popreq); + + return G_SOURCE_REMOVE; +} + +void +dzl_tree_node_show_popover (DzlTreeNode *self, + GtkPopover *popover) +{ + GdkRectangle cell_area; + GdkRectangle visible_rect; + PopupRequest *popreq; + DzlTree *tree; + + g_return_if_fail (DZL_IS_TREE_NODE (self)); + g_return_if_fail (GTK_IS_POPOVER (popover)); + + tree = dzl_tree_node_get_tree (self); + gtk_tree_view_get_visible_rect (GTK_TREE_VIEW (tree), &visible_rect); + dzl_tree_node_get_area (self, &cell_area); + gtk_tree_view_convert_bin_window_to_tree_coords (GTK_TREE_VIEW (tree), + cell_area.x, + cell_area.y, + &cell_area.x, + &cell_area.y); + + popreq = g_slice_new0 (PopupRequest); + popreq->self = g_object_ref (self); + popreq->popover = g_object_ref (popover); + + /* + * If the node is not on screen, we need to animate until we get there. + */ + if ((cell_area.y < visible_rect.y) || + ((cell_area.y + cell_area.height) > + (visible_rect.y + visible_rect.height))) + { + GtkTreePath *path; + + path = dzl_tree_node_get_path (self); + gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (tree), path, NULL, FALSE, 0, 0); + g_clear_pointer (&path, gtk_tree_path_free); + + /* + * FIXME: Time period comes from gtk animation duration. + * Not curently available in pubic API. + * We need to be greater than the max timeout it + * could take to move, since we must have it + * on screen by then. + * + * One alternative might be to check the result + * and if we are still not on screen, then just + * pin it to a row-height from the top or bottom. + */ + g_timeout_add (300, + dzl_tree_node_show_popover_timeout_cb, + popreq); + + return; + } + + dzl_tree_node_show_popover_timeout_cb (g_steal_pointer (&popreq)); +} + +gboolean +_dzl_tree_node_is_dummy (DzlTreeNode *self) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (self), FALSE); + + return self->is_dummy; +} + +gboolean +_dzl_tree_node_get_needs_build_children (DzlTreeNode *self) +{ + g_assert (DZL_IS_TREE_NODE (self)); + + return self->needs_build_children; +} + +void +_dzl_tree_node_set_needs_build_children (DzlTreeNode *self, + gboolean needs_build_children) +{ + g_assert (DZL_IS_TREE_NODE (self)); + + self->needs_build_children = !!needs_build_children; + + if (!needs_build_children) + self->is_dummy = FALSE; +} + +void +_dzl_tree_node_add_dummy_child (DzlTreeNode *self) +{ + GtkTreeStore *model; + DzlTreeNode *dummy; + GtkTreeIter iter; + GtkTreeIter parent; + + g_assert (DZL_IS_TREE_NODE (self)); + + model = _dzl_tree_get_store (self->tree); + dzl_tree_node_get_iter (self, &parent); + dummy = g_object_ref_sink (dzl_tree_node_new ()); + _dzl_tree_node_set_tree (dummy, self->tree); + _dzl_tree_node_set_parent (dummy, self); + dummy->is_dummy = TRUE; + gtk_tree_store_insert_with_values (model, &iter, &parent, -1, + 0, dummy, + -1); + g_clear_object (&dummy); +} + +void +_dzl_tree_node_remove_dummy_child (DzlTreeNode *self) +{ + GtkTreeStore *model; + GtkTreeIter iter; + GtkTreeIter children; + + g_assert (DZL_IS_TREE_NODE (self)); + + if (self->parent == NULL) + return; + + model = _dzl_tree_get_store (self->tree); + + if (dzl_tree_node_get_iter (self, &iter) && + gtk_tree_model_iter_children (GTK_TREE_MODEL (model), &children, &iter)) + { + while (gtk_tree_store_remove (model, &children)) + { /* Do nothing */ } + } +} + +gboolean +dzl_tree_node_get_children_possible (DzlTreeNode *self) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (self), FALSE); + + return self->children_possible; +} + +/** + * dzl_tree_node_set_children_possible: + * @self: A #DzlTreeNode. + * @children_possible: If the node has children. + * + * If the node has not yet been built, setting this to %TRUE will add a + * dummy child node. This dummy node will be removed when when the node + * is built by the registered #DzlTreeBuilder instances. + */ +void +dzl_tree_node_set_children_possible (DzlTreeNode *self, + gboolean children_possible) +{ + g_return_if_fail (DZL_IS_TREE_NODE (self)); + + children_possible = !!children_possible; + + if (children_possible != self->children_possible) + { + self->children_possible = children_possible; + + if (self->tree != NULL && self->needs_build_children) + { + if (self->children_possible) + _dzl_tree_node_add_dummy_child (self); + else + _dzl_tree_node_remove_dummy_child (self); + } + } +} + +gboolean +dzl_tree_node_get_use_dim_label (DzlTreeNode *self) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (self), FALSE); + + return self->use_dim_label; +} + +void +dzl_tree_node_set_use_dim_label (DzlTreeNode *self, + gboolean use_dim_label) +{ + g_return_if_fail (DZL_IS_TREE_NODE (self)); + + use_dim_label = !!use_dim_label; + + if (use_dim_label != self->use_dim_label) + { + self->use_dim_label = use_dim_label; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_USE_DIM_LABEL]); + } +} + +gboolean +dzl_tree_node_is_root (DzlTreeNode *node) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (node), FALSE); + + return node->parent == NULL; +} + +const gchar * +_dzl_tree_node_get_expanded_icon (DzlTreeNode *node) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (node), NULL); + + return g_quark_to_string (node->expanded_icon_name); +} + +gboolean +dzl_tree_node_get_reset_on_collapse (DzlTreeNode *self) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (self), FALSE); + + return self->reset_on_collapse; +} + +void +dzl_tree_node_set_reset_on_collapse (DzlTreeNode *self, + gboolean reset_on_collapse) +{ + g_return_if_fail (DZL_IS_TREE_NODE (self)); + + reset_on_collapse = !!reset_on_collapse; + + if (reset_on_collapse != self->reset_on_collapse) + { + self->reset_on_collapse = reset_on_collapse; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RESET_ON_COLLAPSE]); + } +} + +guint +dzl_tree_node_n_children (DzlTreeNode *self) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (self), 0); + + if (!self->needs_build_children && self->tree != NULL) + { + GtkTreeIter iter; + GtkTreeModel *model; + + model = GTK_TREE_MODEL (_dzl_tree_get_store (self->tree)); + + if (dzl_tree_node_get_iter (self, &iter)) + return gtk_tree_model_iter_n_children (model, &iter); + } + + return 0; +} + +/** + * dzl_tree_node_nth_child: + * @self: a #DzlTreeNode + * @nth: the index of the child + * + * Gets the @nth child of @self or %NULL if it does not exist. + * + * Returns: (transfer full) (nullable): a #DzlTreeNode or %NULL + */ +DzlTreeNode * +dzl_tree_node_nth_child (DzlTreeNode *self, + guint nth) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (self), NULL); + g_return_val_if_fail (!self->needs_build_children, NULL); + + if (self->tree != NULL) + { + GtkTreeIter parent; + GtkTreeIter iter; + GtkTreeModel *model; + + model = GTK_TREE_MODEL (_dzl_tree_get_store (self->tree)); + + if (dzl_tree_node_get_iter (self, &parent) && + gtk_tree_model_iter_nth_child (model, &iter, &parent, nth)) + { + g_autoptr(DzlTreeNode) node = NULL; + + gtk_tree_model_get (model, &iter, 0, &node, -1); + g_assert (DZL_IS_TREE_NODE (node)); + + /* Don't hand back a dummy node */ + if (_dzl_tree_node_is_dummy (node)) + return NULL; + + return g_steal_pointer (&node); + } + } + + return NULL; +} + +/** + * dzl_tree_node_get_foreground_rgba: + * @self: a #DzlTreeNode + * + * Gets the foreground-rgba to use for row text. + * + * If %NULL, the default foreground color should be used. + * + * Returns: (nullable) (transfer none): A #GdkRGBA or %NULL + * + * Since: 3.28 + */ +const GdkRGBA * +dzl_tree_node_get_foreground_rgba (DzlTreeNode *self) +{ + g_return_val_if_fail (DZL_IS_TREE_NODE (self), NULL); + + if (self->foreground_rgba_set) + return &self->foreground_rgba; + + return NULL; +} + +/** + * dzl_tree_node_set_foreground_rgba: + * @self: a #DzlTreeNode + * @foreground_rgba: (nullable): A #GdkRGBA or %NULL + * + * Sets the foreground-rgba to be used by the row text. + * + * If @foreground_rgba is %NULL, the value is reset to the default. + * + * Since: 3.28 + */ +void +dzl_tree_node_set_foreground_rgba (DzlTreeNode *self, + const GdkRGBA *foreground_rgba) +{ + g_return_if_fail (DZL_IS_TREE_NODE (self)); + + if (foreground_rgba != NULL) + self->foreground_rgba = *foreground_rgba; + else + memset (&self->foreground_rgba, 0, sizeof self->foreground_rgba); + + self->foreground_rgba_set = !!foreground_rgba; +} + +/** + * dzl_tree_node_rebuild: + * @self: a #DzlTreeNode + * + * Rebuilds a node, without invalidating children nodes. If you want to + * ensure that children are also rebuilt, use dzl_tree_node_invalidate(). + * + * Since: 3.28 + */ +void +dzl_tree_node_rebuild (DzlTreeNode *self) +{ + DzlTree *tree; + + g_return_if_fail (DZL_IS_TREE_NODE (self)); + + tree = dzl_tree_node_get_tree (self); + + if (tree != NULL) + _dzl_tree_build_node (tree, self); +} diff --git a/src/tree/dzl-tree-node.h b/src/tree/dzl-tree-node.h new file mode 100644 index 0000000..0f0aa7e --- /dev/null +++ b/src/tree/dzl-tree-node.h @@ -0,0 +1,145 @@ +/* dzl-tree-node.h + * + * Copyright (C) 2011-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_TREE_NODE_H +#define DZL_TREE_NODE_H + +#include "dzl-version-macros.h" + +#include "tree/dzl-tree-types.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +DzlTreeNode *dzl_tree_node_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_append (DzlTreeNode *node, + DzlTreeNode *child); +DZL_AVAILABLE_IN_3_28 +void dzl_tree_node_insert (DzlTreeNode *self, + DzlTreeNode *child, + guint position); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_insert_sorted (DzlTreeNode *node, + DzlTreeNode *child, + DzlTreeNodeCompareFunc compare_func, + gpointer user_data); +DZL_AVAILABLE_IN_ALL +gboolean dzl_tree_node_is_root (DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_tree_node_get_icon_name (DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +GObject *dzl_tree_node_get_item (DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +DzlTreeNode *dzl_tree_node_get_parent (DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +GtkTreePath *dzl_tree_node_get_path (DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +gboolean dzl_tree_node_get_iter (DzlTreeNode *node, + GtkTreeIter *iter); +DZL_AVAILABLE_IN_3_28 +guint dzl_tree_node_n_children (DzlTreeNode *self); +DZL_AVAILABLE_IN_3_28 +DzlTreeNode *dzl_tree_node_nth_child (DzlTreeNode *self, + guint nth); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_prepend (DzlTreeNode *node, + DzlTreeNode *child); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_remove (DzlTreeNode *node, + DzlTreeNode *child); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_set_icon_name (DzlTreeNode *node, + const gchar *icon_name); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_set_item (DzlTreeNode *node, + GObject *item); +DZL_AVAILABLE_IN_ALL +gboolean dzl_tree_node_expand (DzlTreeNode *node, + gboolean expand_ancestors); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_collapse (DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_select (DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_get_area (DzlTreeNode *node, + GdkRectangle *area); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_invalidate (DzlTreeNode *node); +DZL_AVAILABLE_IN_3_28 +void dzl_tree_node_rebuild (DzlTreeNode *self); +DZL_AVAILABLE_IN_ALL +gboolean dzl_tree_node_get_expanded (DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_show_popover (DzlTreeNode *node, + GtkPopover *popover); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_tree_node_get_text (DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_set_text (DzlTreeNode *node, + const gchar *text); +DZL_AVAILABLE_IN_ALL +DzlTree *dzl_tree_node_get_tree (DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +gboolean dzl_tree_node_get_children_possible (DzlTreeNode *self); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_set_children_possible (DzlTreeNode *self, + gboolean children_possible); +DZL_AVAILABLE_IN_ALL +gboolean dzl_tree_node_get_use_markup (DzlTreeNode *self); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_set_use_markup (DzlTreeNode *self, + gboolean use_markup); +DZL_AVAILABLE_IN_ALL +GIcon *dzl_tree_node_get_gicon (DzlTreeNode *self); +DZL_AVAILABLE_IN_3_28 +void dzl_tree_node_set_gicon (DzlTreeNode *self, + GIcon *icon); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_add_emblem (DzlTreeNode *self, + const gchar *emblem_name); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_remove_emblem (DzlTreeNode *self, + const gchar *emblem_name); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_clear_emblems (DzlTreeNode *self); +DZL_AVAILABLE_IN_ALL +gboolean dzl_tree_node_has_emblem (DzlTreeNode *self, + const gchar *emblem_name); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_set_emblems (DzlTreeNode *self, + const gchar * const *emblems); +DZL_AVAILABLE_IN_ALL +gboolean dzl_tree_node_get_use_dim_label (DzlTreeNode *self); +DZL_AVAILABLE_IN_ALL +void dzl_tree_node_set_use_dim_label (DzlTreeNode *self, + gboolean use_dim_label); +DZL_AVAILABLE_IN_3_28 +gboolean dzl_tree_node_get_reset_on_collapse (DzlTreeNode *self); +DZL_AVAILABLE_IN_3_28 +void dzl_tree_node_set_reset_on_collapse (DzlTreeNode *self, + gboolean reset_on_collapse); +DZL_AVAILABLE_IN_3_28 +const GdkRGBA *dzl_tree_node_get_foreground_rgba (DzlTreeNode *self); +DZL_AVAILABLE_IN_3_28 +void dzl_tree_node_set_foreground_rgba (DzlTreeNode *self, + const GdkRGBA *foreground_rgba); + +G_END_DECLS + +#endif /* DZL_TREE_NODE_H */ diff --git a/src/tree/dzl-tree-private.h b/src/tree/dzl-tree-private.h new file mode 100644 index 0000000..9a4913e --- /dev/null +++ b/src/tree/dzl-tree-private.h @@ -0,0 +1,118 @@ +/* dzl-tree-private.h + * + * Copyright (C) 2011-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_TREE_PRIVATE_H +#define DZL_TREE_PRIVATE_H + +#include "dzl-tree-types.h" + +G_BEGIN_DECLS + +void _dzl_tree_invalidate (DzlTree *tree, + DzlTreeNode *node); +GtkTreePath *_dzl_tree_get_path (DzlTree *tree, + GList *list); +void _dzl_tree_build_children (DzlTree *self, + DzlTreeNode *node); +void _dzl_tree_build_node (DzlTree *self, + DzlTreeNode *node); +void _dzl_tree_insert (DzlTree *self, + DzlTreeNode *node, + DzlTreeNode *child, + guint position); +void _dzl_tree_append (DzlTree *self, + DzlTreeNode *node, + DzlTreeNode *child); +void _dzl_tree_prepend (DzlTree *self, + DzlTreeNode *node, + DzlTreeNode *child); +void _dzl_tree_insert_sorted (DzlTree *self, + DzlTreeNode *node, + DzlTreeNode *child, + DzlTreeNodeCompareFunc compare_func, + gpointer user_data); +void _dzl_tree_remove (DzlTree *self, + DzlTreeNode *node); +DzlTreeNode *_dzl_tree_get_drop_node (DzlTree *self, + DzlTreeDropPosition *pos); +GPtrArray *_dzl_tree_get_builders (DzlTree *self); +GdkDragAction _dzl_tree_get_drag_action (DzlTree *self); +gboolean _dzl_tree_get_iter (DzlTree *self, + DzlTreeNode *node, + GtkTreeIter *iter); +GtkTreeStore *_dzl_tree_get_store (DzlTree *self); +void _dzl_tree_node_set_tree (DzlTreeNode *node, + DzlTree *tree); +void _dzl_tree_node_set_parent (DzlTreeNode *node, + DzlTreeNode *parent); +const gchar *_dzl_tree_node_get_expanded_icon (DzlTreeNode *node); +gboolean _dzl_tree_node_get_needs_build_children (DzlTreeNode *node); +void _dzl_tree_node_set_needs_build_children (DzlTreeNode *node, + gboolean needs_build_children); +void _dzl_tree_node_add_dummy_child (DzlTreeNode *node); +void _dzl_tree_node_remove_dummy_child (DzlTreeNode *node); +gboolean _dzl_tree_node_is_dummy (DzlTreeNode *self); +void _dzl_tree_builder_set_tree (DzlTreeBuilder *builder, + DzlTree *tree); +void _dzl_tree_builder_added (DzlTreeBuilder *builder, + DzlTree *tree); +void _dzl_tree_builder_removed (DzlTreeBuilder *builder, + DzlTree *tree); +void _dzl_tree_builder_build_node (DzlTreeBuilder *builder, + DzlTreeNode *node); +void _dzl_tree_builder_build_children (DzlTreeBuilder *builder, + DzlTreeNode *node); +gboolean _dzl_tree_builder_drag_data_get (DzlTreeBuilder *builder, + DzlTreeNode *node, + GtkSelectionData *data); +gboolean _dzl_tree_builder_drag_data_received (DzlTreeBuilder *builder, + DzlTreeNode *drop_node, + DzlTreeDropPosition position, + GdkDragAction action, + GtkSelectionData *data); +gboolean _dzl_tree_builder_drag_node_received (DzlTreeBuilder *builder, + DzlTreeNode *drag_node, + DzlTreeNode *drop_node, + DzlTreeDropPosition position, + GdkDragAction action, + GtkSelectionData *data); +gboolean _dzl_tree_builder_drag_node_delete (DzlTreeBuilder *builder, + DzlTreeNode *node); +gboolean _dzl_tree_builder_node_activated (DzlTreeBuilder *builder, + DzlTreeNode *node); +void _dzl_tree_builder_node_popup (DzlTreeBuilder *builder, + DzlTreeNode *node, + GMenu *menu); +void _dzl_tree_builder_node_selected (DzlTreeBuilder *builder, + DzlTreeNode *node); +void _dzl_tree_builder_node_unselected (DzlTreeBuilder *builder, + DzlTreeNode *node); +void _dzl_tree_builder_node_collapsed (DzlTreeBuilder *builder, + DzlTreeNode *node); +void _dzl_tree_builder_node_expanded (DzlTreeBuilder *builder, + DzlTreeNode *node); +gboolean _dzl_tree_builder_node_draggable (DzlTreeBuilder *builder, + DzlTreeNode *node); +gboolean _dzl_tree_builder_node_droppable (DzlTreeBuilder *builder, + DzlTreeNode *node, + GtkSelectionData *data); +GtkTreeStore *_dzl_tree_store_new (DzlTree *tree); + +G_END_DECLS + +#endif /* DZL_TREE_PRIVATE_H */ diff --git a/src/tree/dzl-tree-store.c b/src/tree/dzl-tree-store.c new file mode 100644 index 0000000..f19ea8f --- /dev/null +++ b/src/tree/dzl-tree-store.c @@ -0,0 +1,328 @@ +/* dzl-tree-store.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-tree-store" + +#include "config.h" + +#include "tree/dzl-tree-builder.h" +#include "tree/dzl-tree-node.h" +#include "tree/dzl-tree-private.h" +#include "tree/dzl-tree-store.h" +#include "util/dzl-macros.h" + +struct _DzlTreeStore +{ + GtkTreeStore parent_instance; + + /* Weak references */ + DzlTree *tree; +}; + +static void dest_iface_init (GtkTreeDragDestIface *iface); +static void source_iface_init (GtkTreeDragSourceIface *iface); + +G_DEFINE_TYPE_WITH_CODE (DzlTreeStore, dzl_tree_store, GTK_TYPE_TREE_STORE, + G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_DRAG_DEST, dest_iface_init) + G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_DRAG_SOURCE, source_iface_init)) + +static void +dzl_tree_store_dispose (GObject *object) +{ + DzlTreeStore *self = (DzlTreeStore *)object; + + dzl_clear_weak_pointer (&self->tree); + + G_OBJECT_CLASS (dzl_tree_store_parent_class)->dispose (object); +} + +static void +dzl_tree_store_class_init (DzlTreeStoreClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = dzl_tree_store_dispose; +} + +static void +dzl_tree_store_init (DzlTreeStore *self) +{ + GType types[] = { DZL_TYPE_TREE_NODE }; + + gtk_tree_store_set_column_types (GTK_TREE_STORE (self), 1, types); +} + +static gboolean +dzl_tree_store_row_draggable (GtkTreeDragSource *source, + GtkTreePath *path) +{ + GtkTreeIter iter; + + g_assert (GTK_IS_TREE_DRAG_SOURCE (source)); + g_assert (path != NULL); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (source), &iter, path)) + { + g_autoptr(DzlTreeNode) node = NULL; + GPtrArray *builders; + DzlTree *tree; + + gtk_tree_model_get (GTK_TREE_MODEL (source), &iter, 0, &node, -1); + g_assert (DZL_IS_TREE_NODE (node)); + + tree = dzl_tree_node_get_tree (node); + g_assert (DZL_IS_TREE (tree)); + + builders = _dzl_tree_get_builders (tree); + g_assert (builders != NULL); + + if (dzl_tree_node_is_root (node) || _dzl_tree_node_is_dummy (node)) + return FALSE; + + for (guint i = 0; i < builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (builders, i); + + if (_dzl_tree_builder_node_draggable (builder, node)) + return TRUE; + } + } + + return FALSE; +} + +static gboolean +dzl_tree_store_drag_data_get (GtkTreeDragSource *source, + GtkTreePath *path, + GtkSelectionData *data) +{ + GtkTreeIter iter; + + g_assert (DZL_IS_TREE_STORE (source)); + g_assert (path != NULL); + g_assert (data != NULL); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (source), &iter, path)) + { + g_autoptr(DzlTreeNode) node = NULL; + GPtrArray *builders; + DzlTree *tree; + + gtk_tree_model_get (GTK_TREE_MODEL (source), &iter, 0, &node, -1); + g_assert (DZL_IS_TREE_NODE (node)); + + tree = dzl_tree_node_get_tree (node); + builders = _dzl_tree_get_builders (tree); + + for (guint i = 0; i < builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (builders, i); + + if (_dzl_tree_builder_drag_data_get (builder, node, data)) + return TRUE; + } + } + + return FALSE; +} + +static gboolean +dzl_tree_store_row_drop_possible (GtkTreeDragDest *dest, + GtkTreePath *path, + GtkSelectionData *data) +{ + GtkTreeIter iter; + + g_assert (GTK_IS_TREE_DRAG_DEST (dest)); + g_assert (path != NULL); + g_assert (data != NULL); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (dest), &iter, path)) + { + g_autoptr(DzlTreeNode) node = NULL; + DzlTreeNode *effective; + GPtrArray *builders; + DzlTree *tree; + + gtk_tree_model_get (GTK_TREE_MODEL (dest), &iter, 0, &node, -1); + g_assert (DZL_IS_TREE_NODE (node)); + + tree = dzl_tree_node_get_tree (node); + g_assert (DZL_IS_TREE (tree)); + + builders = _dzl_tree_get_builders (tree); + g_assert (builders != NULL); + + if (dzl_tree_node_is_root (node)) + return FALSE; + + effective = _dzl_tree_node_is_dummy (node) + ? dzl_tree_node_get_parent (node) + : node; + + if (effective == NULL || dzl_tree_node_is_root (effective)) + return FALSE; + + g_assert (effective != NULL); + g_assert (!_dzl_tree_node_is_dummy (effective)); + g_assert (!dzl_tree_node_is_root (effective)); + + for (guint i = 0; i < builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (builders, i); + + if (_dzl_tree_builder_node_droppable (builder, effective, data)) + return TRUE; + } + } + + return FALSE; +} + +static gboolean +dzl_tree_store_drag_data_received (GtkTreeDragDest *dest, + GtkTreePath *path, + GtkSelectionData *data) +{ + DzlTreeStore *self = (DzlTreeStore *)dest; + g_autoptr(DzlTreeNode) drop_node = NULL; + GPtrArray *builders; + DzlTreeDropPosition pos = 0; + GdkDragAction action; + + g_assert (GTK_IS_TREE_DRAG_DEST (self)); + g_assert (self->tree != NULL); + g_assert (path != NULL); + g_assert (data != NULL); + + builders = _dzl_tree_get_builders (self->tree); + drop_node = _dzl_tree_get_drop_node (self->tree, &pos); + action = _dzl_tree_get_drag_action (self->tree); + + /* + * If we have a drag/drop of a node onto/adjacent another node, then + * use the simplified drag_node_received API for the builders instead + * of making them extract the node information themselves. + */ + if (gtk_selection_data_get_target (data) == gdk_atom_intern_static_string ("GTK_TREE_MODEL_ROW")) + { + GtkTreePath *src_path = NULL; + GtkTreeModel *model = NULL; + + if (gtk_tree_get_row_drag_data (data, &model, &src_path)) + { + GtkTreeIter iter; + gboolean found; + + found = gtk_tree_model_get_iter (model, &iter, src_path); + g_clear_pointer (&src_path, gtk_tree_path_free); + + if (found) + { + g_autoptr(DzlTreeNode) drag_node = NULL; + + gtk_tree_model_get (model, &iter, 0, &drag_node, -1); + g_assert (DZL_IS_TREE_NODE (drag_node)); + + for (guint i = 0; i < builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (builders, i); + + g_assert (DZL_IS_TREE_BUILDER (builder)); + + if (_dzl_tree_builder_drag_node_received (builder, drag_node, drop_node, pos, action, data)) + return TRUE; + } + } + } + } + + for (guint i = 0; i < builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (builders, i); + + g_assert (DZL_IS_TREE_BUILDER (builder)); + + if (_dzl_tree_builder_drag_data_received (builder, drop_node, pos, action, data)) + return TRUE; + } + + return FALSE; +} + +static gboolean +dzl_tree_store_drag_data_delete (GtkTreeDragSource *source, + GtkTreePath *path) +{ + DzlTreeStore *self = (DzlTreeStore *)source; + GtkTreeIter iter; + GPtrArray *builders; + + g_assert (GTK_IS_TREE_DRAG_SOURCE (self)); + g_assert (self->tree != NULL); + g_assert (path != NULL); + + builders = _dzl_tree_get_builders (self->tree); + g_assert (builders != NULL); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (source), &iter, path)) + { + g_autoptr(DzlTreeNode) node = NULL; + + gtk_tree_model_get (GTK_TREE_MODEL (source), &iter, 0, &node, -1); + g_assert (DZL_IS_TREE_NODE (node)); + + for (guint i = 0; i < builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (builders, i); + + g_assert (DZL_IS_TREE_BUILDER (builder)); + + if (_dzl_tree_builder_drag_node_delete (builder, node)) + return TRUE; + } + } + + return FALSE; +} + +static void +dest_iface_init (GtkTreeDragDestIface *iface) +{ + iface->row_drop_possible = dzl_tree_store_row_drop_possible; + iface->drag_data_received = dzl_tree_store_drag_data_received; +} + +static void +source_iface_init (GtkTreeDragSourceIface *iface) +{ + iface->row_draggable = dzl_tree_store_row_draggable; + iface->drag_data_get = dzl_tree_store_drag_data_get; + iface->drag_data_delete = dzl_tree_store_drag_data_delete; +} + +GtkTreeStore * +_dzl_tree_store_new (DzlTree *tree) +{ + DzlTreeStore *self; + + self = g_object_new (DZL_TYPE_TREE_STORE, NULL); + dzl_set_weak_pointer (&self->tree, tree); + + return GTK_TREE_STORE (self); +} diff --git a/src/tree/dzl-tree-store.h b/src/tree/dzl-tree-store.h new file mode 100644 index 0000000..333ec8b --- /dev/null +++ b/src/tree/dzl-tree-store.h @@ -0,0 +1,30 @@ +/* dzl-tree-store.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define DZL_TYPE_TREE_STORE (dzl_tree_store_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlTreeStore, dzl_tree_store, DZL, TREE_STORE, GtkTreeStore) + +G_END_DECLS diff --git a/src/tree/dzl-tree-types.h b/src/tree/dzl-tree-types.h new file mode 100644 index 0000000..017f3ea --- /dev/null +++ b/src/tree/dzl-tree-types.h @@ -0,0 +1,52 @@ +/* dzl-tree-types.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_TREE_TYPES_H +#define DZL_TREE_TYPES_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_TREE (dzl_tree_get_type()) +#define DZL_TYPE_TREE_NODE (dzl_tree_node_get_type()) +#define DZL_TYPE_TREE_BUILDER (dzl_tree_builder_get_type()) + +typedef enum +{ + DZL_TREE_DROP_INTO = 0, + DZL_TREE_DROP_BEFORE = 1, + DZL_TREE_DROP_AFTER = 2, +} DzlTreeDropPosition; + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlTree, dzl_tree, DZL, TREE, GtkTreeView) +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlTreeBuilder, dzl_tree_builder, DZL, TREE_BUILDER, GInitiallyUnowned) +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlTreeNode, dzl_tree_node, DZL, TREE_NODE, GInitiallyUnowned) + +typedef gint (*DzlTreeNodeCompareFunc) (DzlTreeNode *a, + DzlTreeNode *b, + gpointer user_data); + +G_END_DECLS + +#endif /* DZL_TREE_TYPES_H */ diff --git a/src/tree/dzl-tree.c b/src/tree/dzl-tree.c new file mode 100644 index 0000000..969a195 --- /dev/null +++ b/src/tree/dzl-tree.c @@ -0,0 +1,2053 @@ +/* dzl-tree.c + * + * Copyright (C) 2011-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-tree" + +#include "config.h" + +#include + +#include "tree/dzl-tree.h" +#include "tree/dzl-tree-node.h" +#include "tree/dzl-tree-private.h" +#include "tree/dzl-tree-store.h" +#include "util/dzl-util-private.h" + +typedef struct +{ + /* Owned references */ + GPtrArray *builders; + DzlTreeNode *root; + GtkTreeStore *store; + GMenuModel *context_menu; + GtkTreePath *last_drop_path; + + /* Unowned references */ + DzlTreeNode *selection; + GtkTreeViewColumn *column; + GtkCellRenderer *cell_pixbuf; + GtkCellRenderer *cell_text; + + GtkTreeViewDropPosition last_drop_pos; + GdkDragAction drag_action; + + GdkRGBA dim_foreground; + + guint show_icons : 1; + guint always_expand : 1; +} DzlTreePrivate; + +typedef struct +{ + gpointer key; + GEqualFunc equal_func; + DzlTreeNode *result; +} NodeLookup; + +typedef struct +{ + DzlTree *self; + DzlTreeFilterFunc filter_func; + gpointer filter_data; + GDestroyNotify filter_data_destroy; +} FilterFunc; + +static void dzl_tree_buildable_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (DzlTree, dzl_tree, GTK_TYPE_TREE_VIEW, + G_ADD_PRIVATE (DzlTree) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, dzl_tree_buildable_init)) + +enum { + PROP_0, + PROP_ALWAYS_EXPAND, + PROP_CONTEXT_MENU, + PROP_ROOT, + PROP_SELECTION, + PROP_SHOW_ICONS, + LAST_PROP +}; + +enum { + ACTION, + POPULATE_POPUP, + LAST_SIGNAL +}; + +static GtkBuildableIface *dzl_tree_parent_buildable_iface; +static GParamSpec *properties [LAST_PROP]; +static guint signals [LAST_SIGNAL]; + +/** + * dzl_tree_get_context_menu: + * + * Returns: (transfer none) (nullable): A #GMenuModel or %NULL. + */ +GMenuModel * +dzl_tree_get_context_menu (DzlTree *self) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TREE (self), NULL); + + return priv->context_menu; +} + +void +dzl_tree_set_context_menu (DzlTree *self, + GMenuModel *model) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (!model || G_IS_MENU_MODEL (model)); + + if (g_set_object (&priv->context_menu, model)) + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONTEXT_MENU]); +} + +void +_dzl_tree_build_node (DzlTree *self, + DzlTreeNode *node) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_assert (DZL_IS_TREE (self)); + g_assert (DZL_IS_TREE_NODE (node)); + + for (guint i = 0; i < priv->builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (priv->builders, i); + + _dzl_tree_builder_build_node (builder, node); + } + + if (!priv->always_expand && + dzl_tree_node_get_children_possible (node) && + dzl_tree_node_n_children (node) == 0) + _dzl_tree_node_add_dummy_child (node); +} + +void +_dzl_tree_build_children (DzlTree *self, + DzlTreeNode *node) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_assert (DZL_IS_TREE (self)); + g_assert (DZL_IS_TREE_NODE (node)); + + _dzl_tree_node_set_needs_build_children (node, FALSE); + _dzl_tree_node_remove_dummy_child (node); + + for (guint i = 0; i < priv->builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (priv->builders, i); + + _dzl_tree_builder_build_children (builder, node); + } +} + +static void +dzl_tree_unselect (DzlTree *self) +{ + GtkTreeSelection *selection; + + g_return_if_fail (DZL_IS_TREE (self)); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self)); + gtk_tree_selection_unselect_all (selection); +} + +static void +dzl_tree_select (DzlTree *self, + DzlTreeNode *node) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeSelection *selection; + GtkTreePath *path; + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + if (priv->selection) + { + dzl_tree_unselect (self); + g_assert (!priv->selection); + } + + priv->selection = node; + + path = dzl_tree_node_get_path (node); + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self)); + gtk_tree_selection_select_path (selection, path); + gtk_tree_path_free (path); +} + +static void +check_visible_foreach (GtkWidget *widget, + gpointer user_data) +{ + gboolean *at_least_one_visible = user_data; + + if (*at_least_one_visible == FALSE) + *at_least_one_visible = gtk_widget_get_visible (widget); +} + +static void +dzl_tree_popup (DzlTree *self, + DzlTreeNode *node, + GdkEventButton *event, + gint target_x, + gint target_y) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + gboolean at_least_one_visible = FALSE; + GtkWidget *menu_widget; + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + if (priv->context_menu != NULL) + { + for (guint i = 0; i < priv->builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (priv->builders, i); + + _dzl_tree_builder_node_popup (builder, node, G_MENU (priv->context_menu)); + } + } + + if (priv->context_menu != NULL) + menu_widget = gtk_menu_new_from_model (G_MENU_MODEL (priv->context_menu)); + else + menu_widget = gtk_menu_new (); + + g_signal_emit (self, signals [POPULATE_POPUP], 0, menu_widget); + + gtk_container_foreach (GTK_CONTAINER (menu_widget), + check_visible_foreach, + &at_least_one_visible); + + if (at_least_one_visible) + { + gtk_menu_attach_to_widget (GTK_MENU (menu_widget), + GTK_WIDGET (self), + NULL); + g_signal_connect_after (menu_widget, + "selection-done", + G_CALLBACK (gtk_widget_destroy), + NULL); + + g_object_set (G_OBJECT (menu_widget), + "rect-anchor-dx", target_x - 12, + "rect-anchor-dy", target_y - 3, + NULL); + gtk_menu_popup_at_widget (GTK_MENU (menu_widget), + GTK_WIDGET (self), + GDK_GRAVITY_NORTH_WEST, + GDK_GRAVITY_NORTH_WEST, + (GdkEvent *)event); + } + else + { + gtk_widget_destroy (menu_widget); + } +} + +static gboolean +dzl_tree_popup_menu (GtkWidget *widget) +{ + DzlTree *self = (DzlTree *)widget; + DzlTreeNode *node; + GdkRectangle area; + + g_assert (DZL_IS_TREE (self)); + + if (!(node = dzl_tree_get_selected (self))) + return FALSE; + + dzl_tree_node_get_area (node, &area); + dzl_tree_popup (self, node, NULL, area.x + area.width, area.y - 1); + + return TRUE; +} + +static void +dzl_tree_selection_changed (DzlTree *self, + GtkTreeSelection *selection) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeModel *model; + DzlTreeNode *unselection; + GtkTreeIter iter; + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (GTK_IS_TREE_SELECTION (selection)); + + /* unowned reference */ + unselection = g_steal_pointer (&priv->selection); + + if (unselection != NULL) + { + for (guint i = 0; i < priv->builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (priv->builders, i); + _dzl_tree_builder_node_unselected (builder, unselection); + } + } + + if (gtk_tree_selection_get_selected (selection, &model, &iter)) + { + g_autoptr(DzlTreeNode) node = NULL; + + gtk_tree_model_get (model, &iter, 0, &node, -1); + + if (node != NULL) + { + for (guint i = 0; i < priv->builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (priv->builders, i); + _dzl_tree_builder_node_selected (builder, node); + } + } + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SELECTION]); +} + +static gboolean +dzl_tree_add_builder_foreach_cb (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data) +{ + g_autoptr(DzlTreeNode) node = NULL; + DzlTreeBuilder *builder = user_data; + DzlTreePrivate *priv; + DzlTree *tree; + + g_return_val_if_fail (GTK_IS_TREE_MODEL (model), FALSE); + g_return_val_if_fail (path != NULL, FALSE); + g_return_val_if_fail (iter != NULL, FALSE); + + tree = dzl_tree_builder_get_tree (builder); + priv = dzl_tree_get_instance_private (tree); + + gtk_tree_model_get (model, iter, 0, &node, -1); + + _dzl_tree_builder_build_node (builder, node); + + if (priv->always_expand || !_dzl_tree_node_get_needs_build_children (node)) + _dzl_tree_builder_build_children (builder, node); + + return FALSE; +} + +static gboolean +dzl_tree_foreach (DzlTree *self, + GtkTreeIter *iter, + GtkTreeModelForeachFunc func, + gpointer user_data) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter child; + gboolean ret; + + g_assert (DZL_IS_TREE (self)); + g_assert (iter != NULL); + g_assert (gtk_tree_store_iter_is_valid (priv->store, iter)); + g_assert (func != NULL); + + model = GTK_TREE_MODEL (priv->store); + path = gtk_tree_model_get_path (model, iter); + ret = func (model, path, iter, user_data); + gtk_tree_path_free (path); + + if (ret) + return TRUE; + + if (gtk_tree_model_iter_children (model, &child, iter)) + { + do + { + if (dzl_tree_foreach (self, &child, func, user_data)) + return TRUE; + } + while (gtk_tree_model_iter_next (model, &child)); + } + + return FALSE; +} + +static void +pixbuf_func (GtkCellLayout *cell_layout, + GtkCellRenderer *cell, + GtkTreeModel *tree_model, + GtkTreeIter *iter, + gpointer data) +{ + g_autoptr(DzlTreeNode) node = NULL; + g_autoptr(GIcon) old_icon = NULL; + DzlTree *self = data; + const gchar *expanded_icon_name; + GIcon *icon; + + g_assert (GTK_IS_CELL_LAYOUT (cell_layout)); + g_assert (GTK_IS_CELL_RENDERER_PIXBUF (cell)); + g_assert (GTK_IS_TREE_MODEL (tree_model)); + g_assert (DZL_IS_TREE (self)); + g_assert (iter != NULL); + + gtk_tree_model_get (tree_model, iter, 0, &node, -1); + + expanded_icon_name = _dzl_tree_node_get_expanded_icon (node); + + if (expanded_icon_name != NULL) + { + GtkTreePath *tree_path; + gboolean expanded; + + tree_path = gtk_tree_model_get_path (tree_model, iter); + expanded = gtk_tree_view_row_expanded (GTK_TREE_VIEW (self), tree_path); + gtk_tree_path_free (tree_path); + + if (expanded) + { + g_object_set (cell, "icon-name", expanded_icon_name, NULL); + return; + } + } + + icon = dzl_tree_node_get_gicon (node); + g_object_get (cell, "gicon", &old_icon, NULL); + if (icon != old_icon || icon == NULL) + g_object_set (cell, "gicon", icon, NULL); +} + +static void +text_func (GtkCellLayout *cell_layout, + GtkCellRenderer *cell, + GtkTreeModel *tree_model, + GtkTreeIter *iter, + gpointer data) +{ + DzlTree *self = data; + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + g_autoptr(DzlTreeNode) node = NULL; + + g_assert (DZL_IS_TREE (self)); + g_assert (GTK_IS_CELL_LAYOUT (cell_layout)); + g_assert (GTK_IS_CELL_RENDERER_TEXT (cell)); + g_assert (GTK_IS_TREE_MODEL (tree_model)); + g_assert (iter != NULL); + + gtk_tree_model_get (tree_model, iter, 0, &node, -1); + + if G_LIKELY (node != NULL) + { + const GdkRGBA *rgba = NULL; + const gchar *text; + gboolean use_markup; + + text = dzl_tree_node_get_text (node); + use_markup = dzl_tree_node_get_use_markup (node); + + if (dzl_tree_node_get_use_dim_label (node)) + rgba = &priv->dim_foreground; + else + rgba = dzl_tree_node_get_foreground_rgba (node); + + g_object_set (cell, + use_markup ? "markup" : "text", text, + "foreground-rgba", rgba, + NULL); + + for (guint i = 0; i < priv->builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (priv->builders, i); + + if (DZL_TREE_BUILDER_GET_CLASS (builder)->cell_data_func) + DZL_TREE_BUILDER_GET_CLASS (builder)->cell_data_func (builder, node, cell); + } + } +} + +void +_dzl_tree_insert (DzlTree *self, + DzlTreeNode *parent, + DzlTreeNode *child, + guint position) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeIter parent_iter; + GtkTreeIter iter; + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (DZL_IS_TREE_NODE (parent)); + g_return_if_fail (DZL_IS_TREE_NODE (child)); + + g_object_ref_sink (child); + + if (dzl_tree_node_get_iter (parent, &parent_iter)) + { + _dzl_tree_node_set_tree (child, self); + _dzl_tree_node_set_parent (child, parent); + + gtk_tree_store_insert_with_values (priv->store, &iter, &parent_iter, position, + 0, child, + -1); + + _dzl_tree_build_node (self, child); + + if (dzl_tree_node_get_children_possible (child)) + _dzl_tree_node_add_dummy_child (child); + + if (priv->always_expand) + { + _dzl_tree_build_children (self, child); + dzl_tree_node_expand (child, TRUE); + } + } + + g_object_unref (child); +} + +static void +dzl_tree_add (DzlTree *self, + DzlTreeNode *node, + DzlTreeNode *child, + gboolean prepend) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeIter *parentptr = NULL; + GtkTreeIter iter; + GtkTreeIter parent; + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + g_return_if_fail (DZL_IS_TREE_NODE (child)); + + _dzl_tree_node_set_tree (child, self); + _dzl_tree_node_set_parent (child, node); + + g_object_ref_sink (child); + + if (node != priv->root) + { + GtkTreePath *path; + + path = dzl_tree_node_get_path (node); + gtk_tree_model_get_iter (GTK_TREE_MODEL (priv->store), &parent, path); + parentptr = &parent; + + g_clear_pointer (&path, gtk_tree_path_free); + } + + gtk_tree_store_insert_with_values (priv->store, &iter, parentptr, + prepend ? 0 : -1, + 0, child, + -1); + + _dzl_tree_build_node (self, child); + + if (dzl_tree_node_get_children_possible (child)) + _dzl_tree_node_add_dummy_child (child); + + if (priv->always_expand) + { + _dzl_tree_build_children (self, child); + dzl_tree_node_expand (child, TRUE); + } + else if (node == priv->root) + { + _dzl_tree_build_children (self, child); + } + + g_object_unref (child); +} + +void +_dzl_tree_insert_sorted (DzlTree *self, + DzlTreeNode *node, + DzlTreeNode *child, + DzlTreeNodeCompareFunc compare_func, + gpointer user_data) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeModel *model; + GtkTreeIter *parent = NULL; + GtkTreeIter node_iter; + GtkTreeIter children; + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + g_return_if_fail (DZL_IS_TREE_NODE (child)); + g_return_if_fail (compare_func != NULL); + + model = GTK_TREE_MODEL (priv->store); + + _dzl_tree_node_set_tree (child, self); + _dzl_tree_node_set_parent (child, node); + _dzl_tree_node_set_needs_build_children (child, TRUE); + + g_object_ref_sink (child); + + if (dzl_tree_node_get_iter (node, &node_iter)) + parent = &node_iter; + + if (gtk_tree_model_iter_children (model, &children, parent)) + { + do + { + g_autoptr(DzlTreeNode) sibling = NULL; + GtkTreeIter that; + + gtk_tree_model_get (model, &children, 0, &sibling, -1); + + g_assert (DZL_IS_TREE_NODE (sibling)); + + if (compare_func (sibling, child, user_data) > 0) + { + gtk_tree_store_insert_before (priv->store, &that, parent, &children); + gtk_tree_store_set (priv->store, &that, 0, child, -1); + goto inserted; + } + } + while (gtk_tree_model_iter_next (model, &children)); + } + + gtk_tree_store_append (priv->store, &children, parent); + gtk_tree_store_set (priv->store, &children, 0, child, -1); + +inserted: + _dzl_tree_build_node (self, child); + + if (priv->always_expand || priv->root == child) + _dzl_tree_build_children (self, child); + + g_object_unref (child); +} + +static void +dzl_tree_row_activated (GtkTreeView *tree_view, + GtkTreePath *path, + GtkTreeViewColumn *column) +{ + DzlTree *self = (DzlTree *)tree_view; + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeModel *model; + GtkTreeIter iter; + gboolean handled = FALSE; + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (path != NULL); + g_return_if_fail (!column || GTK_IS_TREE_VIEW_COLUMN (column)); + + model = gtk_tree_view_get_model (tree_view); + + if (gtk_tree_model_get_iter (model, &iter, path)) + { + g_autoptr(DzlTreeNode) node = NULL; + + gtk_tree_model_get (model, &iter, 0, &node, -1); + g_assert (node != NULL); + g_assert (DZL_IS_TREE_NODE (node)); + + for (guint i = 0; i < priv->builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (priv->builders, i); + + if ((handled = _dzl_tree_builder_node_activated (builder, node))) + break; + } + } + + if (!handled) + { + if (gtk_tree_view_row_expanded (tree_view, path)) + gtk_tree_view_collapse_row (tree_view, path); + else + gtk_tree_view_expand_to_path (tree_view, path); + } +} + +static void +dzl_tree_row_expanded (GtkTreeView *tree_view, + GtkTreeIter *iter, + GtkTreePath *path) +{ + DzlTree *self = (DzlTree *)tree_view; + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + g_autoptr(DzlTreeNode) node = NULL; + GtkTreeModel *model; + + g_assert (DZL_IS_TREE (self)); + g_assert (iter != NULL); + g_assert (path != NULL); + + model = gtk_tree_view_get_model (tree_view); + gtk_tree_model_get (model, iter, 0, &node, -1); + g_assert (node != NULL); + g_assert (DZL_IS_TREE_NODE (node)); + + /* + * If we are expanding a row that has a dummy child, we need to allow + * the builders to add children to the node. + */ + + if (_dzl_tree_node_get_needs_build_children (node)) + { + _dzl_tree_build_children (self, node); + dzl_tree_node_expand (node, FALSE); + dzl_tree_node_select (node); + } + + /* Notify builders of expand */ + for (guint i = 0; i < priv->builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (priv->builders, i); + _dzl_tree_builder_node_expanded (builder, node); + } +} + +static void +dzl_tree_row_collapsed (GtkTreeView *tree_view, + GtkTreeIter *iter, + GtkTreePath *path) +{ + DzlTree *self = (DzlTree *)tree_view; + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + g_autoptr(DzlTreeNode) node = NULL; + GtkTreeModel *model; + + g_assert (DZL_IS_TREE (self)); + g_assert (iter != NULL); + g_assert (path != NULL); + + model = gtk_tree_view_get_model (tree_view); + + /* Ignore things when we are showing a filter. There isn't a whole lot we + * can do here without getting into some weird corner cases, so we'll punt + * until someone really asks for the feature. + */ + if (model != GTK_TREE_MODEL (priv->store)) + return; + + /* Get the node in question */ + gtk_tree_model_get (model, iter, 0, &node, -1); + g_assert (node != NULL); + g_assert (DZL_IS_TREE_NODE (node)); + + /* + * If we are collapsing a row that requests to have its children removed + * and the dummy node re-inserted, go ahead and do so now. + */ + if (dzl_tree_node_get_reset_on_collapse (node)) + { + GtkTreeIter child; + + if (gtk_tree_model_iter_children (model, &child, iter)) + { + while (gtk_tree_store_remove (priv->store, &child)) + { /* Do Nothing */ } + } + + _dzl_tree_node_add_dummy_child (node); + _dzl_tree_node_set_needs_build_children (node, TRUE); + } + + /* Notify builders of collapse */ + for (guint i = 0; i < priv->builders->len; i++) + { + DzlTreeBuilder *builder = g_ptr_array_index (priv->builders, i); + _dzl_tree_builder_node_collapsed (builder, node); + } +} + +static gboolean +dzl_tree_button_press_event (GtkWidget *widget, + GdkEventButton *button) +{ + DzlTree *self = (DzlTree *)widget; + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeIter iter; + gint cell_y; + + g_assert (DZL_IS_TREE (self)); + g_assert (button != NULL); + + if ((button->type == GDK_BUTTON_PRESS) && (button->button == GDK_BUTTON_SECONDARY)) + { + GtkTreePath *tree_path = NULL; + + if (!gtk_widget_has_focus (GTK_WIDGET (self))) + gtk_widget_grab_focus (GTK_WIDGET (self)); + + gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (self), + button->x, + button->y, + &tree_path, + NULL, + NULL, + &cell_y); + + if (tree_path == NULL) + { + dzl_tree_unselect (self); + } + else + { + GtkAllocation alloc; + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (priv->store), &iter, tree_path)) + { + g_autoptr(DzlTreeNode) node = NULL; + + gtk_tree_model_get (GTK_TREE_MODEL (priv->store), &iter, 0, &node, -1); + dzl_tree_select (self, node); + dzl_tree_popup (self, node, button, alloc.x + alloc.width, button->y - cell_y); + } + } + + g_clear_pointer (&tree_path, gtk_tree_path_free); + + return GDK_EVENT_STOP; + } + + return GTK_WIDGET_CLASS (dzl_tree_parent_class)->button_press_event (widget, button); +} + +static gboolean +dzl_tree_find_item_foreach_cb (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data) +{ + g_autoptr(DzlTreeNode) node = NULL; + NodeLookup *lookup = user_data; + gboolean ret = FALSE; + + g_assert (GTK_IS_TREE_MODEL (model)); + g_assert (path != NULL); + g_assert (iter != NULL); + g_assert (lookup != NULL); + + gtk_tree_model_get (model, iter, 0, &node, -1); + + if (node != NULL) + { + GObject *item = dzl_tree_node_get_item (node); + + if (lookup->equal_func (lookup->key, item)) + { + /* We only want a borrowed reference to node */ + lookup->result = node; + ret = TRUE; + } + } + + return ret; +} + +static void +dzl_tree_real_action (DzlTree *self, + const gchar *prefix, + const gchar *action_name, + const gchar *param) +{ + g_autoptr(GVariant) variant = NULL; + g_autofree gchar *name = NULL; + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (action_name != NULL); + + if (*param != 0) + { + GError *error = NULL; + + variant = g_variant_parse (NULL, param, NULL, NULL, &error); + + if (variant == NULL) + { + g_warning ("can't parse keybinding parameters \"%s\": %s", + param, error->message); + g_clear_error (&error); + return; + } + } + + if (prefix) + name = g_strdup_printf ("%s.%s", prefix, action_name); + else + name = g_strdup (action_name); + + dzl_gtk_widget_activate_action (GTK_WIDGET (self), name, variant); +} + +static gboolean +dzl_tree_default_search_equal_func (GtkTreeModel *model, + gint column, + const gchar *key, + GtkTreeIter *iter, + gpointer user_data) +{ + g_autoptr(DzlTreeNode) node = NULL; + gboolean ret = TRUE; + + g_assert (GTK_IS_TREE_MODEL (model)); + g_assert (column == 0); + g_assert (key != NULL); + g_assert (iter != NULL); + + gtk_tree_model_get (model, iter, 0, &node, -1); + + if (node != NULL) + { + const gchar *text = dzl_tree_node_get_text (node); + ret = !(strstr (key, text) != NULL); + } + + return ret; +} + +static void +dzl_tree_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + DzlTree *self = (DzlTree *)buildable; + + g_assert (DZL_IS_TREE (self)); + g_assert (GTK_IS_BUILDER (builder)); + g_assert (G_IS_OBJECT (child)); + + if (g_strcmp0 (type, "builder") == 0) + { + if (!DZL_IS_TREE_BUILDER (child)) + { + g_warning ("Attempt to add invalid builder of type %s to DzlTree.", + g_type_name (G_OBJECT_TYPE (child))); + return; + } + + dzl_tree_add_builder (self, DZL_TREE_BUILDER (child)); + return; + } + + dzl_tree_parent_buildable_iface->add_child (buildable, builder, child, type); +} + +static gboolean +dzl_tree_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time_) +{ + DzlTree *self = (DzlTree *)widget; + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + gboolean ret; + + g_assert (DZL_IS_TREE (self)); + g_assert (GDK_IS_DRAG_CONTEXT (context)); + + ret = GTK_WIDGET_CLASS (dzl_tree_parent_class)->drag_motion (widget, context, x, y, time_); + + /* + * Cache the current drop position so we can use it + * later to determine how to drop on a given node. + */ + g_clear_pointer (&priv->last_drop_path, gtk_tree_path_free); + gtk_tree_view_get_drag_dest_row (GTK_TREE_VIEW (widget), + &priv->last_drop_path, + &priv->last_drop_pos); + + /* Save the drag action for builders dispatch */ + priv->drag_action = gdk_drag_context_get_selected_action (context); + + return ret; +} + +static void +dzl_tree_drag_end (GtkWidget *widget, + GdkDragContext *context) +{ + DzlTree *self = (DzlTree *)widget; + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_assert (DZL_IS_TREE (self)); + g_assert (GDK_IS_DRAG_CONTEXT (context)); + + priv->drag_action = 0; + priv->last_drop_pos = 0; + g_clear_pointer (&priv->last_drop_path, gtk_tree_path_free); + + GTK_WIDGET_CLASS (dzl_tree_parent_class)->drag_end (widget, context); +} + +DzlTreeNode * +_dzl_tree_get_drop_node (DzlTree *self, + DzlTreeDropPosition *pos) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + g_autoptr(DzlTreeNode) node = NULL; + GtkTreeModel *model; + GtkTreeIter iter; + DzlTreeDropPosition dummy_pos; + + g_return_val_if_fail (DZL_IS_TREE (self), NULL); + + if (pos == NULL) + pos = &dummy_pos; + + *pos = 0; + + /* We can't do anything if we don't have a path */ + if (priv->last_drop_path == NULL) + return NULL; + + /* We don't have anything to do if path doesn't exist */ + model = gtk_tree_view_get_model (GTK_TREE_VIEW (self)); + if (!gtk_tree_model_get_iter (model, &iter, priv->last_drop_path)) + return NULL; + + gtk_tree_model_get (model, &iter, 0, &node, -1); + g_assert (node != NULL); + g_assert (DZL_IS_TREE_NODE (node)); + + switch (priv->last_drop_pos) + { + case GTK_TREE_VIEW_DROP_BEFORE: + *pos = DZL_TREE_DROP_BEFORE; + break; + + case GTK_TREE_VIEW_DROP_AFTER: + *pos = DZL_TREE_DROP_AFTER; + break; + + case GTK_TREE_VIEW_DROP_INTO_OR_BEFORE: + case GTK_TREE_VIEW_DROP_INTO_OR_AFTER: + *pos = DZL_TREE_DROP_INTO; + break; + + default: + break; + } + + return g_steal_pointer (&node); +} + +static void +dzl_tree_style_updated (GtkWidget *widget) +{ + DzlTree *self = (DzlTree *)widget; + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkStyleContext *style_context; + + g_assert (DZL_IS_TREE (self)); + + GTK_WIDGET_CLASS (dzl_tree_parent_class)->style_updated (widget); + + style_context = gtk_widget_get_style_context (widget); + gtk_style_context_save (style_context); + gtk_style_context_add_class (style_context, "dim-label"); + gtk_style_context_get_color (style_context, + gtk_style_context_get_state (style_context), + &priv->dim_foreground); + gtk_style_context_restore (style_context); +} + +static void +dzl_tree_destroy (GtkWidget *widget) +{ + DzlTree *self = (DzlTree *)widget; + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_assert (DZL_IS_TREE (self)); + + g_clear_pointer (&priv->last_drop_path, gtk_tree_path_free); + g_clear_pointer (&priv->builders, g_ptr_array_unref); + g_clear_object (&priv->store); + g_clear_object (&priv->root); + g_clear_object (&priv->context_menu); + + GTK_WIDGET_CLASS (dzl_tree_parent_class)->destroy (widget); +} + +static void +dzl_tree_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlTree *self = DZL_TREE (object); + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + switch (prop_id) + { + case PROP_ALWAYS_EXPAND: + g_value_set_boolean (value, priv->always_expand); + break; + + case PROP_CONTEXT_MENU: + g_value_set_object (value, priv->context_menu); + break; + + case PROP_ROOT: + g_value_set_object (value, priv->root); + break; + + case PROP_SELECTION: + g_value_set_object (value, priv->selection); + break; + + case PROP_SHOW_ICONS: + g_value_set_boolean (value, priv->show_icons); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_tree_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlTree *self = DZL_TREE (object); + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + switch (prop_id) + { + case PROP_ALWAYS_EXPAND: + priv->always_expand = g_value_get_boolean (value); + break; + + case PROP_CONTEXT_MENU: + dzl_tree_set_context_menu (self, g_value_get_object (value)); + break; + + case PROP_ROOT: + dzl_tree_set_root (self, g_value_get_object (value)); + break; + + case PROP_SELECTION: + dzl_tree_select (self, g_value_get_object (value)); + break; + + case PROP_SHOW_ICONS: + dzl_tree_set_show_icons (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_tree_buildable_init (GtkBuildableIface *iface) +{ + dzl_tree_parent_buildable_iface = g_type_interface_peek_parent (iface); + + iface->add_child = dzl_tree_add_child; +} + +static void +dzl_tree_class_init (DzlTreeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass); + + object_class->get_property = dzl_tree_get_property; + object_class->set_property = dzl_tree_set_property; + + widget_class->button_press_event = dzl_tree_button_press_event; + widget_class->destroy = dzl_tree_destroy; + widget_class->popup_menu = dzl_tree_popup_menu; + widget_class->style_updated = dzl_tree_style_updated; + + widget_class->drag_motion = dzl_tree_drag_motion; + widget_class->drag_end = dzl_tree_drag_end; + + tree_view_class->row_activated = dzl_tree_row_activated; + tree_view_class->row_expanded = dzl_tree_row_expanded; + tree_view_class->row_collapsed = dzl_tree_row_collapsed; + + klass->action = dzl_tree_real_action; + + properties [PROP_ALWAYS_EXPAND] = + g_param_spec_boolean ("always-expand", + "Always expand", + "Always expand", + FALSE, + (G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + properties[PROP_CONTEXT_MENU] = + g_param_spec_object ("context-menu", + "Context Menu", + "The context menu to display", + G_TYPE_MENU_MODEL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + properties[PROP_ROOT] = + g_param_spec_object ("root", + "Root", + "The root object of the tree", + DZL_TYPE_TREE_NODE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + properties[PROP_SELECTION] = + g_param_spec_object ("selection", + "Selection", + "The node selection", + DZL_TYPE_TREE_NODE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + properties [PROP_SHOW_ICONS] = + g_param_spec_boolean ("show-icons", + "Show Icons", + "Show Icons", + FALSE, + (G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + signals [ACTION] = + g_signal_new ("action", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (DzlTreeClass, action), + NULL, NULL, NULL, + G_TYPE_NONE, + 3, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_STRING); + + signals [POPULATE_POPUP] = + g_signal_new ("populate-popup", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlTreeClass, populate_popup), + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + GTK_TYPE_WIDGET); +} + +static void +dzl_tree_init (DzlTree *self) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeSelection *selection; + GtkCellRenderer *cell; + GtkCellLayout *column; + + priv->builders = g_ptr_array_new (); + g_ptr_array_set_free_func (priv->builders, g_object_unref); + priv->store = _dzl_tree_store_new (self); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self)); + g_signal_connect_object (selection, "changed", + G_CALLBACK (dzl_tree_selection_changed), + self, + G_CONNECT_SWAPPED); + + column = g_object_new (GTK_TYPE_TREE_VIEW_COLUMN, + "title", "Node", + NULL); + priv->column = GTK_TREE_VIEW_COLUMN (column); + + cell = g_object_new (GTK_TYPE_CELL_RENDERER_PIXBUF, + "xpad", 3, + "visible", priv->show_icons, + NULL); + priv->cell_pixbuf = cell; + g_object_bind_property (self, "show-icons", cell, "visible", 0); + gtk_cell_layout_pack_start (column, cell, FALSE); + gtk_cell_layout_set_cell_data_func (column, cell, pixbuf_func, self, NULL); + + cell = g_object_new (GTK_TYPE_CELL_RENDERER_TEXT, + "ellipsize", PANGO_ELLIPSIZE_NONE, + NULL); + priv->cell_text = cell; + gtk_cell_layout_pack_start (column, cell, TRUE); + gtk_cell_layout_set_cell_data_func (column, cell, text_func, self, NULL); + + gtk_tree_view_append_column (GTK_TREE_VIEW (self), + GTK_TREE_VIEW_COLUMN (column)); + + gtk_tree_view_set_model (GTK_TREE_VIEW (self), + GTK_TREE_MODEL (priv->store)); + + gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW (self), + dzl_tree_default_search_equal_func, + NULL, NULL); + gtk_tree_view_set_search_column (GTK_TREE_VIEW (self), 0); +} + +void +dzl_tree_expand_to_node (DzlTree *self, + DzlTreeNode *node) +{ + g_assert (DZL_IS_TREE (self)); + g_assert (DZL_IS_TREE_NODE (node)); + + if (dzl_tree_node_get_expanded (node)) + { + dzl_tree_node_expand (node, TRUE); + } + else + { + dzl_tree_node_expand (node, TRUE); + dzl_tree_node_collapse (node); + } +} + +gboolean +dzl_tree_get_show_icons (DzlTree *self) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TREE (self), FALSE); + + return priv->show_icons; +} + +void +dzl_tree_set_show_icons (DzlTree *self, + gboolean show_icons) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_return_if_fail (DZL_IS_TREE (self)); + + show_icons = !!show_icons; + + if (show_icons != priv->show_icons) + { + priv->show_icons = show_icons; + g_object_set (priv->cell_pixbuf, "visible", show_icons, NULL); + /* + * WORKAROUND: + * + * Changing the visibility of the cell does not force a redraw of the + * tree view. So to force it, we will hide/show our entire pixbuf/text + * column. + */ + gtk_tree_view_column_set_visible (priv->column, FALSE); + gtk_tree_view_column_set_visible (priv->column, TRUE); + g_object_notify_by_pspec (G_OBJECT (self), + properties [PROP_SHOW_ICONS]); + } +} + +/** + * dzl_tree_get_selected: + * @self: (in): A #DzlTree. + * + * Gets the currently selected node in the tree. + * + * Returns: (transfer none): A #DzlTreeNode. + */ +DzlTreeNode * +dzl_tree_get_selected (DzlTree *self) +{ + GtkTreeSelection *selection; + GtkTreeModel *model; + GtkTreeIter iter; + DzlTreeNode *ret = NULL; + + g_return_val_if_fail (DZL_IS_TREE (self), NULL); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self)); + if (gtk_tree_selection_get_selected (selection, &model, &iter)) + { + gtk_tree_model_get (model, &iter, 0, &ret, -1); + + /* + * We incurred an extra reference when extracting the value from + * the treemodel. Since it owns the reference, we can drop it here + * so that we don't transfer the ownership to the caller. + */ + g_object_unref (ret); + } + + return ret; +} + +/** + * dzl_tree_unselect_all: + * @self: (in): A #DzlTree. + * + * Unselects the currently selected node in the tree. + */ +void +dzl_tree_unselect_all (DzlTree *self) +{ + GtkTreeSelection *selection; + + g_return_if_fail (DZL_IS_TREE (self)); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self)); + gtk_tree_selection_unselect_all (selection); +} + +void +dzl_tree_scroll_to_node (DzlTree *self, + DzlTreeNode *node) +{ + GtkTreePath *path; + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + path = dzl_tree_node_get_path (node); + gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (self), path, NULL, FALSE, 0, 0); + gtk_tree_path_free (path); +} + +GtkTreePath * +_dzl_tree_get_path (DzlTree *self, + GList *list) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeModel *model; + GtkTreeIter iter; + GtkTreeIter *iter_ptr; + GList *list_iter; + + g_assert (DZL_IS_TREE (self)); + + model = GTK_TREE_MODEL (priv->store); + + if ((list == NULL) || (list->data != priv->root) || (list->next == NULL)) + return NULL; + + iter_ptr = NULL; + + for (list_iter = list->next; list_iter; list_iter = list_iter->next) + { + GtkTreeIter children; + + if (gtk_tree_model_iter_children (model, &children, iter_ptr)) + { + gboolean found = FALSE; + + do + { + g_autoptr(DzlTreeNode) item = NULL; + + gtk_tree_model_get (model, &children, 0, &item, -1); + found = (item == (DzlTreeNode *)list_iter->data); + } + while (!found && gtk_tree_model_iter_next (model, &children)); + + if (found) + { + iter = children; + iter_ptr = &iter; + continue; + } + } + + return NULL; + } + + return gtk_tree_model_get_path (model, &iter); +} + +/** + * dzl_tree_add_builder: + * @self: A #DzlTree. + * @builder: A #DzlTreeBuilder to add. + * + * Add a builder to the tree. + */ +void +dzl_tree_add_builder (DzlTree *self, + DzlTreeBuilder *builder) +{ + GtkTreeIter iter; + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (DZL_IS_TREE_BUILDER (builder)); + + g_ptr_array_add (priv->builders, g_object_ref_sink (builder)); + + _dzl_tree_builder_set_tree (builder, self); + _dzl_tree_builder_added (builder, self); + + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->store), &iter)) + dzl_tree_foreach (self, &iter, dzl_tree_add_builder_foreach_cb, builder); +} + +/** + * dzl_tree_remove_builder: + * @self: (in): A #DzlTree. + * @builder: (in): A #DzlTreeBuilder to remove. + * + * Removes a builder from the tree. + */ +void +dzl_tree_remove_builder (DzlTree *self, + DzlTreeBuilder *builder) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + gsize i; + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (DZL_IS_TREE_BUILDER (builder)); + + for (i = 0; i < priv->builders->len; i++) + { + if (builder == g_ptr_array_index (priv->builders, i)) + { + g_object_ref (builder); + g_ptr_array_remove_index (priv->builders, i); + _dzl_tree_builder_removed (builder, self); + g_object_unref (builder); + } + } +} + +/** + * dzl_tree_get_root: + * + * Retrieves the root node of the tree. The root node is not a visible node + * in the self, but a placeholder for all other builders to build upon. + * + * Returns: (transfer none) (nullable): A #DzlTreeNode or %NULL. + */ +DzlTreeNode * +dzl_tree_get_root (DzlTree *self) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TREE (self), NULL); + + return priv->root; +} + +/** + * dzl_tree_set_root: + * @self: A #DzlTree. + * @node: A #DzlTreeNode. + * + * Sets the root node of the #DzlTree widget. This is used to build + * the items within the treeview. The item itself will not be added + * to the self, but the direct children will be. + */ +void +dzl_tree_set_root (DzlTree *self, + DzlTreeNode *root) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_return_if_fail (DZL_IS_TREE (self)); + + if (priv->root != root) + { + GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self)); + GtkTreeModel *current; + + if (selection != NULL) + gtk_tree_selection_unselect_all (selection); + + if (priv->root != NULL) + { + _dzl_tree_node_set_parent (priv->root, NULL); + _dzl_tree_node_set_tree (priv->root, NULL); + gtk_tree_store_clear (priv->store); + g_clear_object (&priv->root); + } + + current = gtk_tree_view_get_model (GTK_TREE_VIEW (self)); + if (GTK_IS_TREE_MODEL_FILTER (current)) + gtk_tree_model_filter_clear_cache (GTK_TREE_MODEL_FILTER (current)); + + if (root != NULL) + { + priv->root = g_object_ref_sink (root); + _dzl_tree_node_set_parent (priv->root, NULL); + _dzl_tree_node_set_tree (priv->root, self); + _dzl_tree_build_children (self, priv->root); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ROOT]); + } +} + +void +dzl_tree_rebuild (DzlTree *self) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeSelection *selection; + + g_return_if_fail (DZL_IS_TREE (self)); + + /* + * We don't want notification of selection changes while rebuilding. + */ + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self)); + gtk_tree_selection_unselect_all (selection); + + if (priv->root != NULL) + { + gtk_tree_store_clear (priv->store); + _dzl_tree_build_children (self, priv->root); + } +} + +/** + * dzl_tree_find_custom: + * @self: A #DzlTree + * @equal_func: (scope call): A #GEqualFunc + * @key: the key for @equal_func + * + * Walks the entire tree looking for the first item that matches given + * @equal_func and @key. + * + * The first parameter to @equal_func will always be @key. + * The second parameter will be the nodes #DzlTreeNode:item property. + * + * Returns: (nullable) (transfer none): A #DzlTreeNode or %NULL. + */ +DzlTreeNode * +dzl_tree_find_custom (DzlTree *self, + GEqualFunc equal_func, + gpointer key) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + NodeLookup lookup; + + g_return_val_if_fail (DZL_IS_TREE (self), NULL); + g_return_val_if_fail (equal_func != NULL, NULL); + + lookup.key = key; + lookup.equal_func = equal_func; + lookup.result = NULL; + + gtk_tree_model_foreach (GTK_TREE_MODEL (priv->store), + dzl_tree_find_item_foreach_cb, + &lookup); + + return lookup.result; +} + +/** + * dzl_tree_find_item: + * @self: A #DzlTree. + * @item: (allow-none): A #GObject or %NULL. + * + * Finds a #DzlTreeNode with an item property matching @item. + * + * Returns: (transfer none) (nullable): A #DzlTreeNode or %NULL. + */ +DzlTreeNode * +dzl_tree_find_item (DzlTree *self, + GObject *item) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + NodeLookup lookup; + + g_return_val_if_fail (DZL_IS_TREE (self), NULL); + g_return_val_if_fail (!item || G_IS_OBJECT (item), NULL); + + lookup.key = item; + lookup.equal_func = g_direct_equal; + lookup.result = NULL; + + gtk_tree_model_foreach (GTK_TREE_MODEL (priv->store), + dzl_tree_find_item_foreach_cb, + &lookup); + + return lookup.result; +} + +void +_dzl_tree_append (DzlTree *self, + DzlTreeNode *node, + DzlTreeNode *child) +{ + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + g_return_if_fail (DZL_IS_TREE_NODE (child)); + + dzl_tree_add (self, node, child, FALSE); +} + +void +_dzl_tree_prepend (DzlTree *self, + DzlTreeNode *node, + DzlTreeNode *child) +{ + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + g_return_if_fail (DZL_IS_TREE_NODE (child)); + + dzl_tree_add (self, node, child, TRUE); +} + +void +_dzl_tree_invalidate (DzlTree *self, + DzlTreeNode *node) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeModel *model; + GtkTreePath *path; + DzlTreeNode *parent; + GtkTreeIter iter; + GtkTreeIter child; + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + model = GTK_TREE_MODEL (priv->store); + path = dzl_tree_node_get_path (node); + + if (path != NULL) + { + + if (gtk_tree_model_get_iter (model, &iter, path)) + { + if (gtk_tree_model_iter_children (model, &child, &iter)) + { + while (gtk_tree_store_remove (priv->store, &child)) + { /* Do nothing */ } + } + } + + gtk_tree_path_free (path); + } + + _dzl_tree_node_set_needs_build_children (node, TRUE); + + parent = dzl_tree_node_get_parent (node); + + /* Build the node (unless it's the root */ + if (parent != NULL) + _dzl_tree_build_node (self, node); + + /* Now build the children if necessary */ + if ((parent == NULL) || dzl_tree_node_get_expanded (parent)) + _dzl_tree_build_children (self, node); +} + +/** + * dzl_tree_find_child_node: + * @self: A #DzlTree + * @node: A #DzlTreeNode + * @find_func: (scope call): A callback to locate the child + * @user_data: user data for @find_func + * + * Searches through the direct children of @node for a matching child. + * @find_func should return %TRUE if the child matches, otherwise %FALSE. + * + * Returns: (transfer none) (nullable): A #DzlTreeNode or %NULL. + */ +DzlTreeNode * +dzl_tree_find_child_node (DzlTree *self, + DzlTreeNode *node, + DzlTreeFindFunc find_func, + gpointer user_data) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreeModel *model; + GtkTreePath *path = NULL; + DzlTreeNode *ret = NULL; + GtkTreeIter iter; + GtkTreeIter children; + + g_return_val_if_fail (DZL_IS_TREE (self), NULL); + g_return_val_if_fail (!node || DZL_IS_TREE_NODE (node), NULL); + g_return_val_if_fail (find_func, NULL); + + if (node == NULL) + node = priv->root; + + if (node == NULL) + { + g_warning ("Cannot find node. No root node has been set on %s.", + g_type_name (G_OBJECT_TYPE (self))); + return NULL; + } + + if (_dzl_tree_node_get_needs_build_children (node)) + _dzl_tree_build_children (self, node); + + model = GTK_TREE_MODEL (priv->store); + path = dzl_tree_node_get_path (node); + + if (path != NULL) + { + if (!gtk_tree_model_get_iter (model, &iter, path)) + goto finish; + + if (!gtk_tree_model_iter_children (model, &children, &iter)) + goto finish; + } + else + { + if (!gtk_tree_model_iter_children (model, &children, NULL)) + goto finish; + } + + do + { + g_autoptr(DzlTreeNode) child = NULL; + + gtk_tree_model_get (model, &children, 0, &child, -1); + + if (find_func (self, node, child, user_data)) + { + /* + * We want to returned a borrowed reference to the child node but + * we got a full reference when calling gtk_tree_model_get(). + * It is safe to unref the child here before we return as long + * as the item is in our model. + */ + ret = child; + goto finish; + } + } + while (gtk_tree_model_iter_next (model, &children)); + +finish: + g_clear_pointer (&path, gtk_tree_path_free); + + return ret; +} + +void +_dzl_tree_remove (DzlTree *self, + DzlTreeNode *node) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreePath *path; + GtkTreeIter iter; + + g_return_if_fail (DZL_IS_TREE (self)); + g_return_if_fail (DZL_IS_TREE_NODE (node)); + + path = dzl_tree_node_get_path (node); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (priv->store), &iter, path)) + gtk_tree_store_remove (priv->store, &iter); + + gtk_tree_path_free (path); +} + +gboolean +_dzl_tree_get_iter (DzlTree *self, + DzlTreeNode *node, + GtkTreeIter *iter) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + GtkTreePath *path; + gboolean ret = FALSE; + + g_return_val_if_fail (DZL_IS_TREE (self), FALSE); + g_return_val_if_fail (DZL_IS_TREE_NODE (node), FALSE); + g_return_val_if_fail (iter, FALSE); + + path = dzl_tree_node_get_path (node); + + if (path != NULL) + { + ret = gtk_tree_model_get_iter (GTK_TREE_MODEL (priv->store), iter, path); + gtk_tree_path_free (path); + } + + return ret; +} + +static void +filter_func_free (gpointer user_data) +{ + FilterFunc *data = user_data; + + if (data->filter_data_destroy) + data->filter_data_destroy (data->filter_data); + + g_free (data); +} + +static gboolean +dzl_tree_model_filter_recursive (GtkTreeModel *model, + GtkTreeIter *parent, + FilterFunc *filter) +{ + GtkTreeIter child; + + if (gtk_tree_model_iter_children (model, &child, parent)) + { + do + { + g_autoptr(DzlTreeNode) node = NULL; + + gtk_tree_model_get (model, &child, 0, &node, -1); + + if (node != NULL) + { + if (filter->filter_func (filter->self, node, filter->filter_data)) + return TRUE; + + if (!_dzl_tree_node_get_needs_build_children (node)) + { + if (dzl_tree_model_filter_recursive (model, &child, filter)) + return TRUE; + } + } + } + while (gtk_tree_model_iter_next (model, &child)); + } + + return FALSE; +} + +static gboolean +dzl_tree_model_filter_visible_func (GtkTreeModel *model, + GtkTreeIter *iter, + gpointer data) +{ + g_autoptr(DzlTreeNode) node = NULL; + FilterFunc *filter = data; + gboolean ret; + + g_assert (filter != NULL); + g_assert (DZL_IS_TREE (filter->self)); + g_assert (filter->filter_func != NULL); + + /* + * This is a rather complex situation. + * + * We might not match, but one of our children nodes might match. + * Furthering the issue, the children might still need to be built. + * For some cases, this could be really expensive (think file tree) + * but for other things, it could be cheap. If you are going to use + * a filter func for your tree, you probably should avoid being + * too lazy and ensure the nodes are available. + * + * Therefore, we will only check available nodes, and ignore the + * case where the children nodes need to be built. * + * + * TODO: Another option would be to iteratively build the items after + * the initial filter. + */ + + gtk_tree_model_get (model, iter, 0, &node, -1); + ret = filter->filter_func (filter->self, node, filter->filter_data); + + /* Short circuit if we already matched. */ + if (ret) + return TRUE; + + /* If any of our children match, we should match. */ + if (dzl_tree_model_filter_recursive (model, iter, filter)) + return TRUE; + + return FALSE; +} + +/** + * dzl_tree_set_filter: + * @self: A #DzlTree + * @filter_func: (scope notified): A callback to determien visibility. + * @filter_data: User data for @filter_func. + * @filter_data_destroy: Destroy notify for @filter_data. + * + * Sets the filter function to be used to determine visability of a tree node. + */ +void +dzl_tree_set_filter (DzlTree *self, + DzlTreeFilterFunc filter_func, + gpointer filter_data, + GDestroyNotify filter_data_destroy) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_return_if_fail (DZL_IS_TREE (self)); + + if (filter_func == NULL) + { + gtk_tree_view_set_model (GTK_TREE_VIEW (self), GTK_TREE_MODEL (priv->store)); + } + else + { + FilterFunc *data; + GtkTreeModel *filter; + + data = g_new0 (FilterFunc, 1); + data->self = self; + data->filter_func = filter_func; + data->filter_data = filter_data; + data->filter_data_destroy = filter_data_destroy; + + filter = gtk_tree_model_filter_new (GTK_TREE_MODEL (priv->store), NULL); + gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (filter), + dzl_tree_model_filter_visible_func, + data, + filter_func_free); + gtk_tree_view_set_model (GTK_TREE_VIEW (self), GTK_TREE_MODEL (filter)); + g_clear_object (&filter); + } +} + +GtkTreeStore * +_dzl_tree_get_store (DzlTree *self) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TREE (self), NULL); + + return priv->store; +} + +GPtrArray * +_dzl_tree_get_builders (DzlTree *self) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TREE (self), NULL); + + return priv->builders; +} + +GdkDragAction +_dzl_tree_get_drag_action (DzlTree *self) +{ + DzlTreePrivate *priv = dzl_tree_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_TREE (self), 0); + + return priv->drag_action; +} diff --git a/src/tree/dzl-tree.h b/src/tree/dzl-tree.h new file mode 100644 index 0000000..07da343 --- /dev/null +++ b/src/tree/dzl-tree.h @@ -0,0 +1,124 @@ +/* dzl-tree.h + * + * Copyright (C) 2011-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_TREE_H +#define DZL_TREE_H + +#include + +#include "dzl-version-macros.h" + +#include "tree/dzl-tree-builder.h" +#include "tree/dzl-tree-node.h" +#include "tree/dzl-tree-types.h" + +G_BEGIN_DECLS + +/** + * DzlTreeFindFunc: + * + * Callback to check @child, a child of @node, matches a lookup + * request. Returns %TRUE if @child matches, %FALSE if not. + * + * Returns: %TRUE if @child matched + */ +typedef gboolean (*DzlTreeFindFunc) (DzlTree *tree, + DzlTreeNode *node, + DzlTreeNode *child, + gpointer user_data); + +/** + * DzlTreeFilterFunc: + * + * Callback to check if @node should be visible. + * + * Returns: %TRUE if @node should be visible. + */ +typedef gboolean (*DzlTreeFilterFunc) (DzlTree *tree, + DzlTreeNode *node, + gpointer user_data); + +struct _DzlTreeClass +{ + GtkTreeViewClass parent_class; + + void (*action) (DzlTree *self, + const gchar *action_group, + const gchar *action_name, + const gchar *param); + void (*populate_popup) (DzlTree *self, + GtkWidget *widget); + + /*< private >*/ + gpointer _padding[12]; +}; + +DZL_AVAILABLE_IN_ALL +void dzl_tree_add_builder (DzlTree *self, + DzlTreeBuilder *builder); +DZL_AVAILABLE_IN_ALL +void dzl_tree_remove_builder (DzlTree *self, + DzlTreeBuilder *builder); +DZL_AVAILABLE_IN_ALL +DzlTreeNode *dzl_tree_find_item (DzlTree *self, + GObject *item); +DZL_AVAILABLE_IN_ALL +DzlTreeNode *dzl_tree_find_custom (DzlTree *self, + GEqualFunc equal_func, + gpointer key); +DZL_AVAILABLE_IN_ALL +DzlTreeNode *dzl_tree_get_selected (DzlTree *self); +DZL_AVAILABLE_IN_ALL +void dzl_tree_unselect_all (DzlTree *self); +DZL_AVAILABLE_IN_ALL +void dzl_tree_rebuild (DzlTree *self); +DZL_AVAILABLE_IN_ALL +void dzl_tree_set_root (DzlTree *self, + DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +DzlTreeNode *dzl_tree_get_root (DzlTree *self); +DZL_AVAILABLE_IN_ALL +void dzl_tree_set_show_icons (DzlTree *self, + gboolean show_icons); +DZL_AVAILABLE_IN_ALL +gboolean dzl_tree_get_show_icons (DzlTree *self); +DZL_AVAILABLE_IN_ALL +void dzl_tree_scroll_to_node (DzlTree *self, + DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +void dzl_tree_expand_to_node (DzlTree *self, + DzlTreeNode *node); +DZL_AVAILABLE_IN_ALL +DzlTreeNode *dzl_tree_find_child_node (DzlTree *self, + DzlTreeNode *node, + DzlTreeFindFunc find_func, + gpointer user_data); +DZL_AVAILABLE_IN_ALL +void dzl_tree_set_filter (DzlTree *self, + DzlTreeFilterFunc filter_func, + gpointer filter_data, + GDestroyNotify filter_data_destroy); +DZL_AVAILABLE_IN_ALL +GMenuModel *dzl_tree_get_context_menu (DzlTree *self); +DZL_AVAILABLE_IN_ALL +void dzl_tree_set_context_menu (DzlTree *self, + GMenuModel *context_menu); + +G_END_DECLS + +#endif /* DZL_TREE_H */ diff --git a/src/tree/meson.build b/src/tree/meson.build new file mode 100644 index 0000000..222eca8 --- /dev/null +++ b/src/tree/meson.build @@ -0,0 +1,25 @@ +tree_headers = [ + 'dzl-tree-builder.h', + 'dzl-tree-node.h', + 'dzl-tree-types.h', + 'dzl-tree.h', + 'dzl-list-store-adapter.h', +] + +tree_sources = [ + 'dzl-tree-builder.c', + 'dzl-tree.c', + 'dzl-tree-node.c', + 'dzl-tree-store.c', + 'dzl-list-store-adapter.c', +] + +tree_enums_headers = [ + 'dzl-tree-types.h', +] + +libdazzle_public_headers += files(tree_headers) +libdazzle_public_sources += files(tree_sources) +dzl_enum_headers += files(tree_enums_headers) + +install_headers(tree_headers, subdir: join_paths(libdazzle_header_subdir, 'tree')) diff --git a/src/util/dzl-cairo.c b/src/util/dzl-cairo.c new file mode 100644 index 0000000..fcc390c --- /dev/null +++ b/src/util/dzl-cairo.c @@ -0,0 +1,88 @@ +/* dzl-cairo.c + * + * Copyright (C) 2014-2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-cairo" + +#include "config.h" + +#include "dzl-cairo.h" + +cairo_region_t * +dzl_cairo_region_create_from_clip_extents (cairo_t *cr) +{ + cairo_rectangle_int_t crect; + GdkRectangle rect; + + g_return_val_if_fail (cr, NULL); + + gdk_cairo_get_clip_rectangle (cr, &rect); + crect.x = rect.x; + crect.y = rect.y; + crect.width = rect.width; + crect.height = rect.height; + + return cairo_region_create_rectangle (&crect); +} + +void +dzl_cairo_rounded_rectangle (cairo_t *cr, + const GdkRectangle *rect, + gint x_radius, + gint y_radius) +{ + gint x; + gint y; + gint width; + gint height; + gint x1, x2; + gint y1, y2; + gint xr1, xr2; + gint yr1, yr2; + + g_return_if_fail (cr); + g_return_if_fail (rect); + + x = rect->x; + y = rect->y; + width = rect->width; + height = rect->height; + + x1 = x; + x2 = x1 + width; + y1 = y; + y2 = y1 + height; + + x_radius = MIN (x_radius, width / 2.0); + y_radius = MIN (y_radius, width / 2.0); + + xr1 = x_radius; + xr2 = x_radius / 2.0; + yr1 = y_radius; + yr2 = y_radius / 2.0; + + cairo_move_to (cr, x1 + xr1, y1); + cairo_line_to (cr, x2 - xr1, y1); + cairo_curve_to (cr, x2 - xr2, y1, x2, y1 + yr2, x2, y1 + yr1); + cairo_line_to (cr, x2, y2 - yr1); + cairo_curve_to (cr, x2, y2 - yr2, x2 - xr2, y2, x2 - xr1, y2); + cairo_line_to (cr, x1 + xr1, y2); + cairo_curve_to (cr, x1 + xr2, y2, x1, y2 - yr2, x1, y2 - yr1); + cairo_line_to (cr, x1, y1 + yr1); + cairo_curve_to (cr, x1, y1 + yr2, x1 + xr2, y1, x1 + xr1, y1); + cairo_close_path (cr); +} diff --git a/src/util/dzl-cairo.h b/src/util/dzl-cairo.h new file mode 100644 index 0000000..1afd7c2 --- /dev/null +++ b/src/util/dzl-cairo.h @@ -0,0 +1,72 @@ +/* dzl-cairo.h + * + * Copyright (C) 2014 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_CAIRO_H +#define DZL_CAIRO_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +cairo_region_t *dzl_cairo_region_create_from_clip_extents (cairo_t *cr); +DZL_AVAILABLE_IN_ALL +void dzl_cairo_rounded_rectangle (cairo_t *cr, + const GdkRectangle *rect, + gint x_radius, + gint y_radius); + +static inline gboolean +dzl_cairo_rectangle_x2 (const cairo_rectangle_int_t *rect) +{ + return rect->x + rect->width; +} + +static inline gboolean +dzl_cairo_rectangle_y2 (const cairo_rectangle_int_t *rect) +{ + return rect->y + rect->height; +} + +static inline gboolean +dzl_cairo_rectangle_center (const cairo_rectangle_int_t *rect) +{ + return rect->x + (rect->width/2); +} + +static inline gboolean +dzl_cairo_rectangle_middle (const cairo_rectangle_int_t *rect) +{ + return rect->y + (rect->height/2); +} + +static inline cairo_bool_t +dzl_cairo_rectangle_contains_rectangle (const cairo_rectangle_int_t *a, + const cairo_rectangle_int_t *b) +{ + return (a->x <= b->x && + a->x + (int) a->width >= b->x + (int) b->width && + a->y <= b->y && + a->y + (int) a->height >= b->y + (int) b->height); +} + +G_END_DECLS + +#endif /* DZL_CAIRO_H */ diff --git a/src/util/dzl-cancellable.c b/src/util/dzl-cancellable.c new file mode 100644 index 0000000..c59de83 --- /dev/null +++ b/src/util/dzl-cancellable.c @@ -0,0 +1,190 @@ +/* dzl-cancellable.c + * + * Copyright © 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-cancellable" + +#include "config.h" + +#include + +#include "util/dzl-cancellable.h" +#include "util/dzl-macros.h" + +#define CHAINED_INFO_MAGIC 0x81734637 + +typedef struct +{ + guint magic; + volatile gint ref_count; + GMutex mutex; + GWeakRef self; + GWeakRef other; + gulong other_handler; +} ChainedInfo; + +static void +chained_info_free (gpointer data) +{ + ChainedInfo *info = data; + g_autoptr(GCancellable) self = NULL; + g_autoptr(GCancellable) other = NULL; + + g_assert (info != NULL); + g_assert (info->magic == CHAINED_INFO_MAGIC); + g_assert (info->ref_count == 0); + + info->magic = 0; + + self = g_weak_ref_get (&info->self); + other = g_weak_ref_get (&info->other); + + if (other != NULL && info->other_handler != 0) + dzl_clear_signal_handler (other, &info->other_handler); + else + info->other_handler = 0; + + g_weak_ref_clear (&info->other); + g_weak_ref_clear (&info->self); + + g_mutex_clear (&info->mutex); + + g_slice_free (ChainedInfo, info); +} + +static void +chained_info_unref (ChainedInfo *info) +{ + g_autoptr(GCancellable) other = NULL; + + g_assert (info != NULL); + g_assert (info->ref_count > 0); + g_assert (info->magic == CHAINED_INFO_MAGIC); + + if ((other = g_weak_ref_get (&info->other))) + { + gulong handler_id; + + g_mutex_lock (&info->mutex); + handler_id = info->other_handler; + info->other_handler = 0; + g_mutex_unlock (&info->mutex); + + if (handler_id) + g_signal_handler_disconnect (other, handler_id); + } + + if (g_atomic_int_dec_and_test (&info->ref_count)) + chained_info_free (info); +} + +static void +dzl_cancellable_cancelled_cb (GCancellable *other, + ChainedInfo *info) +{ + g_autoptr(GCancellable) self = NULL; + + g_assert (G_IS_CANCELLABLE (other)); + g_assert (info != NULL); + g_assert (info->ref_count > 0); + g_assert (info->magic == CHAINED_INFO_MAGIC); + + self = g_weak_ref_get (&info->self); + + if (self != NULL) + { + if (!g_cancellable_is_cancelled (self)) + g_cancellable_cancel (self); + } + + dzl_clear_signal_handler (other, &info->other_handler); +} + +static void +dzl_cancellable_weak_cb (gpointer data, + GObject *where_object_was) +{ + ChainedInfo *info = data; + + g_assert (info != NULL); + g_assert (info->ref_count > 0); + g_assert (info->magic == CHAINED_INFO_MAGIC); + + chained_info_unref (info); +} + +/** + * dzl_cancellable_chain: + * @self: (nullable): a #GCancellable or %NULL + * @other: (nullable): a #GCancellable or %NULL + * + * If both @self and @other are not %NULL, then the cancellation of + * @other will be propagated to @self if @other is cancelled. + * + * If @self and @other are the same, @self is returned and no additional + * chaining will occur. + * + * If @self and @other are %NULL, then %NULL is returned. + * If @self is non-%NULL, it will be returned. + * If @self is %NULL and @other is non-%NULL, other will be + * returned. This is useful to succinctly chain cancellables like: + * + * |[ + * cancellable = dzl_cancellable_chain (cancellable, self->cancellable); + * ]| + * + * Returns: (transfer none) (nullable): a #GCancellable or %NULL + * + * Since: 3.28 + */ +GCancellable * +dzl_cancellable_chain (GCancellable *self, + GCancellable *other) +{ + ChainedInfo *info; + + g_return_val_if_fail (!self || G_IS_CANCELLABLE (self), NULL); + g_return_val_if_fail (!other || G_IS_CANCELLABLE (other), NULL); + + if (self == other) + return self; + else if (self == NULL) + return other; + else if (other == NULL) + return self; + + /* + * We very much want to avoid taking a reference in the process + * here because that makes it difficult to know if we've created + * any sort of reference cycles or cancellable leaks. + */ + + info = g_slice_new0 (ChainedInfo); + info->magic = CHAINED_INFO_MAGIC; + info->ref_count = 3; + g_mutex_init (&info->mutex); + g_weak_ref_init (&info->self, self); + g_weak_ref_init (&info->other, other); + g_object_weak_ref (G_OBJECT (self), dzl_cancellable_weak_cb, info); + g_object_weak_ref (G_OBJECT (other), dzl_cancellable_weak_cb, info); + info->other_handler = g_cancellable_connect (other, + G_CALLBACK (dzl_cancellable_cancelled_cb), + info, + (GDestroyNotify)chained_info_unref); + + return self; +} diff --git a/src/util/dzl-cancellable.h b/src/util/dzl-cancellable.h new file mode 100644 index 0000000..fd99026 --- /dev/null +++ b/src/util/dzl-cancellable.h @@ -0,0 +1,31 @@ +/* dzl-cancellable.h + * + * Copyright © 2017 Christian Hergert + * + * 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 . + */ + +#pragma once + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_3_28 +GCancellable *dzl_cancellable_chain (GCancellable *self, + GCancellable *other); + +G_END_DECLS diff --git a/src/util/dzl-counter.c b/src/util/dzl-counter.c new file mode 100644 index 0000000..9f707b9 --- /dev/null +++ b/src/util/dzl-counter.c @@ -0,0 +1,655 @@ +/* dzl-counter.c + * + * Copyright (C) 2013-2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#include "config.h" + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef G_OS_UNIX +# include +#endif + +#include "dzl-counter.h" + +G_DEFINE_BOXED_TYPE (DzlCounterArena, dzl_counter_arena, dzl_counter_arena_ref, dzl_counter_arena_unref) + +#define MAX_COUNTERS 2000 +#define NAME_FORMAT "/DzlCounters-%u" +#define MAGIC 0x71167125 +#define COUNTER_MAX_SHM (1024 * 1024 * 4) +#define COUNTERS_PER_GROUP 8 +#define DATA_CELL_SIZE 64 +#define CELLS_PER_INFO (sizeof(CounterInfo) / DATA_CELL_SIZE) +#define CELLS_PER_HEADER 2 +#define CELLS_PER_GROUP(ncpu) \ + (((sizeof (CounterInfo) * COUNTERS_PER_GROUP) + \ + (sizeof(DzlCounterValue) * (ncpu))) / DATA_CELL_SIZE) +#define DZL_MEMORY_BARRIER __sync_synchronize() + +typedef struct +{ + guint cell : 29; /* Counter groups starting cell */ + guint position : 3; /* Index within counter group */ + gchar category[20]; /* Counter category name. */ + gchar name[32]; /* Counter name. */ + gchar description[72]; /* Counter description */ +} CounterInfo __attribute__((aligned (DATA_CELL_SIZE))); + +G_STATIC_ASSERT (sizeof (CounterInfo) == 128); +G_STATIC_ASSERT (CELLS_PER_INFO == 2); +G_STATIC_ASSERT (CELLS_PER_GROUP(1) == 17); +G_STATIC_ASSERT (CELLS_PER_GROUP(2) == 18); +G_STATIC_ASSERT (CELLS_PER_GROUP(4) == 20); +G_STATIC_ASSERT (CELLS_PER_GROUP(8) == 24); +G_STATIC_ASSERT (CELLS_PER_GROUP(16) == 32); + +typedef struct +{ + gint64 values[8]; +} DataCell __attribute__((aligned (DATA_CELL_SIZE))); + +G_STATIC_ASSERT (sizeof (DataCell) == 64); + +typedef struct +{ + guint32 magic; /* Expected magic value */ + guint32 size; /* Size of underlying shm file */ + guint32 ncpu; /* Number of CPUs registered with */ + guint32 first_offset; /* Offset to first counter info in cells */ + guint32 n_counters; /* Number of CounterInfos */ + gchar padding [108]; +} ShmHeader __attribute__((aligned (DATA_CELL_SIZE))); + +G_STATIC_ASSERT (sizeof(ShmHeader) == (DATA_CELL_SIZE * CELLS_PER_HEADER)); + +struct _DzlCounterArena +{ + gint ref_count; + guint arena_is_malloced : 1; + guint data_is_mmapped : 1; + guint is_local_arena : 1; + gsize n_cells; + DataCell *cells; + gsize data_length; + GPid pid; + guint n_counters; + GList *counters; +}; + +G_LOCK_DEFINE_STATIC (reglock); + +static void _dzl_counter_init_getcpu (void) __attribute__ ((constructor)); +static guint (*_dzl_counter_getcpu_helper) (void); + +gint64 +dzl_counter_get (DzlCounter *counter) +{ + gint64 value = 0; + guint ncpu; + guint i; + + g_return_val_if_fail (counter, G_GINT64_CONSTANT (-1)); + + ncpu = g_get_num_processors (); + + DZL_MEMORY_BARRIER; + + for (i = 0; i < ncpu; i++) + value += counter->values [i].value; + + return value; +} + +void +dzl_counter_reset (DzlCounter *counter) +{ + guint ncpu; + guint i; + + g_return_if_fail (counter); + + ncpu = g_get_num_processors (); + + for (i = 0; i < ncpu; i++) + counter->values [i].value = 0; + + DZL_MEMORY_BARRIER; +} + +static void +_dzl_counter_arena_atexit (void) +{ + gchar name [32]; + gint pid; + + pid = getpid (); + g_snprintf (name, sizeof name, NAME_FORMAT, pid); + shm_unlink (name); +} + +static void +_dzl_counter_arena_init_local (DzlCounterArena *arena) +{ + gsize size; + gint page_size; + ShmHeader *header; +#ifndef G_OS_WIN32 + gpointer mem; + unsigned pid; + gint fd; + gchar name [32]; +#endif + + page_size = sysconf (_SC_PAGE_SIZE); + + /* Implausible, but squashes warnings. */ + if (page_size < 4096) + { + page_size = 4096; + size = page_size * 4; + goto use_malloc; + } + +#ifndef G_OS_WIN32 + + /* + * FIXME: https://bugzilla.gnome.org/show_bug.cgi?id=749280 + * + * We have some very tricky work ahead of us to add unlimited numbers + * of counters at runtime. We basically need to avoid placing counters + * that could overlap a page. + */ + size = page_size * 4; + + arena->ref_count = 1; + arena->is_local_arena = TRUE; + + if (getenv ("DZL_COUNTER_DISABLE_SHM")) + goto use_malloc; + + pid = getpid (); + g_snprintf (name, sizeof name, NAME_FORMAT, pid); + + if (-1 == (fd = shm_open (name, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR|S_IRGRP))) + goto use_malloc; + + /* + * ftruncate() will cause reads to be zero. Therefore, we don't need to + * do write() of zeroes to initialize the shared memory area. + */ + if (-1 == ftruncate (fd, size)) + goto failure; + + /* + * Memory map the shared memory segement so that we can store our counters + * within it. We need to layout the counters into the segment so that other + * processes can traverse and read the values by loading the shared page. + */ + mem = mmap (NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); + if (mem == MAP_FAILED) + goto failure; + + close (fd); + atexit (_dzl_counter_arena_atexit); + + arena->data_is_mmapped = TRUE; + arena->cells = mem; + arena->n_cells = (size / DATA_CELL_SIZE); + arena->data_length = size; + + header = mem; + header->magic = MAGIC; + header->ncpu = g_get_num_processors (); + header->first_offset = CELLS_PER_HEADER; + + DZL_MEMORY_BARRIER; + + header->size = (guint32)arena->data_length; + + return; + +failure: + shm_unlink (name); + close (fd); +#endif + +use_malloc: + g_warning ("Failed to allocate shared memory for counters. " + "Counters will not be available to external processes."); + + /* + * Ask for double memory than required so that we can be certain + * that the memalign will keep us within valid memory ranges. + */ + if (size < page_size) + size = page_size; + arena->data_is_mmapped = FALSE; + arena->n_cells = (size / DATA_CELL_SIZE); + arena->data_length = size; +#ifdef G_OS_WIN32 + arena->cells = _aligned_malloc (size, page_size); +#else + arena->cells = g_malloc0 (size << 1); +#endif + +#ifndef G_OS_WIN32 + /* + * Make sure that we have a properly aligned allocation back from + * malloc. Since we are at least a page size, we should pretty much + * be guaranteed this, but better to check with posix_memalign(). + */ + if (posix_memalign ((gpointer)&arena->cells, page_size, size << 1) != 0) + { + perror ("posix_memalign()"); + abort (); + } +#endif + + header = (void *)arena->cells; + header->magic = MAGIC; + header->ncpu = g_get_num_processors (); + header->first_offset = CELLS_PER_HEADER; + + DZL_MEMORY_BARRIER; + + header->size = (guint32)arena->data_length; +} + +static gboolean +_dzl_counter_arena_init_remote (DzlCounterArena *arena, + GPid pid) +{ + ShmHeader header; + gssize count; + gchar name [32]; + void *mem = NULL; + guint ncpu; + guint n_counters; + guint i; + int fd = -1; + + g_assert (arena != NULL); + + ncpu = g_get_num_processors (); + + arena->ref_count = 1; + arena->pid = pid; + + g_snprintf (name, sizeof name, NAME_FORMAT, (int)pid); + + fd = shm_open (name, O_RDONLY, 0); + if (fd < 0) + return FALSE; + + count = pread (fd, &header, sizeof header, 0); + + if ((count != sizeof header) || + (header.magic != MAGIC) || + (header.size > COUNTER_MAX_SHM) || + (header.ncpu > g_get_num_processors ())) + goto failure; + + n_counters = header.n_counters; + + if (n_counters > MAX_COUNTERS) + goto failure; + + if (header.size < + CELLS_PER_HEADER + (((n_counters / COUNTERS_PER_GROUP) + 1) * CELLS_PER_GROUP(header.ncpu))) + goto failure; + + mem = mmap (NULL, header.size, PROT_READ, MAP_SHARED, fd, 0); + + if (mem == MAP_FAILED) + goto failure; + + arena->is_local_arena = FALSE; + arena->data_is_mmapped = TRUE; + arena->cells = mem; + arena->n_cells = header.size / DATA_CELL_SIZE; + arena->data_length = header.size; + arena->counters = NULL; + + /* Not strictly required, but helpful for now */ + if (header.first_offset != CELLS_PER_HEADER) + goto failure; + + for (i = 0; i < n_counters; i++) + { + CounterInfo *info; + DzlCounter *counter; + guint group_start_cell; + guint group; + guint position; + + group = i / COUNTERS_PER_GROUP; + position = i % COUNTERS_PER_GROUP; + group_start_cell = header.first_offset + (CELLS_PER_GROUP (ncpu) * group); + + if (group_start_cell + CELLS_PER_GROUP (ncpu) >= arena->n_cells) + goto failure; + + info = &(((CounterInfo *)&arena->cells[group_start_cell])[position]); + + counter = g_new0 (DzlCounter, 1); + counter->category = g_strndup (info->category, sizeof info->category); + counter->name = g_strndup (info->name, sizeof info->name); + counter->description = g_strndup (info->description, sizeof info->description); + counter->values = (DzlCounterValue *)&arena->cells [info->cell].values[info->position]; + +#if 0 + g_print ("Counter discovered: cell=%u position=%u category=%s name=%s values=%p offset=%lu\n", + info->cell, info->position, info->category, info->name, counter->values, + (guint8*)counter->values - (guint8*)mem); +#endif + + arena->counters = g_list_prepend (arena->counters, counter); + } + + close (fd); + + return TRUE; + +failure: + close (fd); + + if ((mem != NULL) && (mem != MAP_FAILED)) + munmap (mem, header.size); + + return FALSE; +} + +DzlCounterArena * +dzl_counter_arena_new_for_pid (GPid pid) +{ + DzlCounterArena *arena; + + arena = g_new0 (DzlCounterArena, 1); + + if (!_dzl_counter_arena_init_remote (arena, pid)) + { + g_free (arena); + return NULL; + } + + return arena; +} + +static void +_dzl_counter_arena_destroy (DzlCounterArena *arena) +{ + g_assert (arena != NULL); + + if (arena->data_is_mmapped) + munmap (arena->cells, arena->data_length); + else +#ifdef G_OS_WIN32 + /* Allocated with _aligned_malloc() */ + _aligned_free (arena->cells); +#else + g_free (arena->cells); +#endif + + g_clear_pointer (&arena->counters, g_list_free); + + arena->cells = NULL; + + if (arena->arena_is_malloced) + g_free (arena); +} + +DzlCounterArena * +dzl_counter_arena_get_default (void) +{ + static DzlCounterArena instance; + static gsize initialized; + + if (G_UNLIKELY (g_once_init_enter (&initialized))) + { + _dzl_counter_arena_init_local (&instance); + g_once_init_leave (&initialized, 1); + } + + return &instance; +} + +DzlCounterArena * +dzl_counter_arena_ref (DzlCounterArena *arena) +{ + g_return_val_if_fail (arena, NULL); + g_return_val_if_fail (arena->ref_count > 0, NULL); + + g_atomic_int_inc (&arena->ref_count); + + return arena; +} + +void +dzl_counter_arena_unref (DzlCounterArena *arena) +{ + g_return_if_fail (arena); + g_return_if_fail (arena->ref_count); + + if (g_atomic_int_dec_and_test (&arena->ref_count)) + _dzl_counter_arena_destroy (arena); +} + +/** + * dzl_counter_arena_foreach: + * @arena: An #DzlCounterArena + * @func: (scope call): A callback to execute + * @user_data: user data for @func + * + * Calls @func for every counter found in @area. + */ +void +dzl_counter_arena_foreach (DzlCounterArena *arena, + DzlCounterForeachFunc func, + gpointer user_data) +{ + GList *iter; + + g_return_if_fail (arena != NULL); + g_return_if_fail (func != NULL); + + for (iter = arena->counters; iter; iter = iter->next) + func (iter->data, user_data); +} + +void +dzl_counter_arena_register (DzlCounterArena *arena, + DzlCounter *counter) +{ + CounterInfo *info; + guint group; + guint ncpu; + guint position; + guint group_start_cell; + + g_return_if_fail (arena != NULL); + g_return_if_fail (counter != NULL); + + if (!arena->is_local_arena) + { + g_warning ("Cannot add counters to a remote arena."); + return; + } + + ncpu = g_get_num_processors (); + + G_LOCK (reglock); + + /* + * Get the counter group and position within the group of the counter. + */ + group = arena->n_counters / COUNTERS_PER_GROUP; + position = arena->n_counters % COUNTERS_PER_GROUP; + + /* + * Get the starting cell for this group. Cells roughly map to cachelines. + */ + group_start_cell = CELLS_PER_HEADER + (CELLS_PER_GROUP (ncpu) * group); + info = &((CounterInfo *)&arena->cells [group_start_cell])[position]; + + g_assert (position < COUNTERS_PER_GROUP); + g_assert (group_start_cell < arena->n_cells); + + /* + * Store information about the counter in the SHM area. Also, update + * the counter values pointer to map to the right cell in the SHM zone. + */ + info->cell = group_start_cell + (COUNTERS_PER_GROUP * CELLS_PER_INFO); + info->position = position; + g_snprintf (info->category, sizeof info->category, "%s", counter->category); + g_snprintf (info->description, sizeof info->description, "%s", counter->description); + g_snprintf (info->name, sizeof info->name, "%s", counter->name); + counter->values = (DzlCounterValue *)&arena->cells [info->cell].values[info->position]; + +#if 0 + g_print ("Counter registered: cell=%u position=%u category=%s name=%s\n", + info->cell, info->position, info->category, info->name); +#endif + + /* + * Track the counter address, so we can _foreach() them. + */ + arena->counters = g_list_append (arena->counters, counter); + arena->n_counters++; + + /* + * Now notify remote processes of the counter. + */ + DZL_MEMORY_BARRIER; + ((ShmHeader *)&arena->cells[0])->n_counters++; + + G_UNLOCK (reglock); +} + +#ifdef __linux__ +static void * +_dzl_counter_find_getcpu_in_vdso (void) +{ + static const gchar *vdso_names[] = { + "linux-vdso.so.1", + "linux-vdso32.so.1", + "linux-vdso64.so.1", + NULL + }; + static const gchar *sym_names[] = { + "__kernel_getcpu", + "__vdso_getcpu", + NULL + }; + gint i; + + for (i = 0; vdso_names [i]; i++) + { + GModule *lib; + gint j; + + lib = g_module_open (vdso_names [i], 0); + if (lib == NULL) + continue; + + for (j = 0; sym_names [j]; j++) + { + void *sym = NULL; + + if (!g_module_symbol (lib, sym_names [j], &sym) || (sym == NULL)) + continue; + + return sym; + } + + g_module_close (lib); + } + + return NULL; +} + +static guint (*_dzl_counter_getcpu_vdso_raw) (int *cpu, + int *node, + void *tcache); + +static guint +_dzl_counter_getcpu_vdso_helper (void) +{ + int cpu; + _dzl_counter_getcpu_vdso_raw (&cpu, NULL, NULL); + return cpu; +} +#endif + +#ifndef HAVE_SCHED_GETCPU +static guint +_dzl_counter_getcpu_fallback (void) +{ + return 0; +} +#endif + +#ifdef DZL_HAVE_RDTSCP +static guint +_dzl_counter_getcpu_rdtscp (void) +{ + return dzl_get_current_cpu_rdtscp (); +} +#endif + +static void +_dzl_counter_init_getcpu (void) +{ + +#ifdef DZL_HAVE_RDTSCP + _dzl_counter_getcpu_helper = _dzl_counter_getcpu_rdtscp; +#endif + +#ifdef __linux__ + _dzl_counter_getcpu_vdso_raw = _dzl_counter_find_getcpu_in_vdso (); + + if (_dzl_counter_getcpu_vdso_raw) + { + _dzl_counter_getcpu_helper = _dzl_counter_getcpu_vdso_helper; + return; + } +#endif + +#ifdef HAVE_SCHED_GETCPU + _dzl_counter_getcpu_helper = (guint (*) (void))sched_getcpu; +#else + _dzl_counter_getcpu_helper = _dzl_counter_getcpu_fallback; +#endif +} + +guint +dzl_get_current_cpu_call (void) +{ + return _dzl_counter_getcpu_helper (); +} diff --git a/src/util/dzl-counter.h b/src/util/dzl-counter.h new file mode 100644 index 0000000..498b937 --- /dev/null +++ b/src/util/dzl-counter.h @@ -0,0 +1,301 @@ +/* dzl-counter.h + * + * Copyright (C) 2013-2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + * + * Additionally, this file does not claim copyright over the expansion + * of macros in your source program. + */ + +#ifndef DZL_COUNTER_H +#define DZL_COUNTER_H + +#include + +#include "dzl-version-macros.h" + +/* + * History + * ======= + * + * DzlCounter is a performance counter based on ideas from previous work + * on high performance counters. They are not guaranteed to be 100% + * correct, but they approach that with no synchronization given new + * enough hardware. In particular, we use %ecx from rdtscp (the core id) + * to determine which cachline to increment the counter within. + * + * Given a counter, the value will be split up int NCPU cachelines where + * NCPU is the number of cores returned from get_nprocs() (on Linux). + * + * Updating the counter is very cheap, reading back the counter requires + * a volatile read of each cacheline. Again, no correctness is guaranteed. + * + * In practice, very few values are lost even during tight competing loops. + * A loss can happen when the thread is pre-empted between the %rdtscp + * instruction and the addq increment (on x86_64). + * + * + * Using DzlCounter + * ================ + * + * To define a counter, you must have support for constructor attributes. + * + * DZL_DEFINE_COUNTER (Symbol, "Category", "Name", "Description") + * + * To increment the counter in a function of your choice (but within the + * same module), use DZL_COUNTER_ADD, DZL_COUNTER_INC, DZL_COUNTER_DEC. + * + * DZL_COUNTER_INC (Symbol); + * + * + * Architecture Support + * ==================== + * + * If you are not on x86_64, or are missing the rdtscp instruction, a 64-bit + * atomic will be performed using __sync_fetch_and_add8(). Clearly, we can + * do some more work here to abstract which implementation is used, but we + * only support GCC and Clang today, which both have that intrinsic. Some + * architectures may not have it (such as 32-bit PPC), but I'm not too + * concerned about that at the moment. + * + * The counters are mapped into a shared memory zone using shm_open() and + * mmap(). An external program can then discover the available counters + * and print them without blocking the target program. It simply must + * perform the reads in a volatile manner just like the target process + * would need to do for readback. + * + * DzlCounterArena provides a helper to walk through the counters in the + * shared memory zone. dzl_counter_arena_foreach(). + * + * You cannot remove a counter once it has been registered. + * + * + * Accessing Counters Remotely + * =========================== + * + * You can access the counters from out of process. By opening the SHM zone + * and reading the contents from each cachline, you can get the approximate + * state of the target application without blocking it. + * + * DzlCounterArena provides a helper for you to do this. + * + * DzlCounterArena *arena; + * + * arena = dzl_counter_arena_new_for_pid (other_process_pid); + * dzl_counter_arena_foreach (arena, my_counter_callback, user_data); + * + * + * Data Layout + * =========== + * + * The layout of the shared memory zone is broken into "cells". Each cell + * is an approximate cacheline (64-bytes) on modern Intel hardware. Indexes + * to data locations are represented in cells to simplify the math and + * allow the compiler to know we are working with properly aligned structures. + * + * The base pointer in DzlCounter.values is not 64-byte aligned! It is 8-byte + * aligned and points to the offset within the cacheline for that counter. + * We pack 8 64-bit counters into a single cacheline. This allows us to avoid + * an extra MOV instruction when incrementing since we only need to perform + * the offset from the base pointer. + * + * The first two cells are the header which contain information about the + * underlying shm file and how large the mmap() range should be. + * + * After that, begin the counters. + * + * The counters are layed out in groups of 8 counters. + * + * [8 CounterInfo Structs (128-bytes each)][N_CPU Data Zones (64-byte each)] + * + * See dzl-counter.c for more information on the contents of these structures. + * + * + * Build System Requirements + * ========================= + * + * We need to know if rdtscp is available at compile time. In an effort + * to keep the headers as portable as possible (if that matters here?) we + * require that you define DZL_HAVE_RDTSCP if the instruction is supported. + * + * An example for autoconf might be similar to the following: + * + * AC_MSG_CHECKING([for fast counters with rdtscp]) + * AC_RUN_IFELSE( + * [AC_LANG_SOURCE([[ + * #include + * int main (int argc, char *argv[]) { int cpu; __builtin_ia32_rdtscp (&cpu); return 0; }]])], + * [have_rdtscp=yes], + * [have_rdtscp=no]) + * AC_MSG_RESULT([$have_rdtscp]) + * AS_IF([test "$have_rdtscp" = "yes"], + * [CFLAGS="$CFLAGS -DDZL_HAVE_RDTSCP"]) + */ + +G_BEGIN_DECLS + +#ifdef DZL_HAVE_RDTSCP +# include + static inline guint + dzl_get_current_cpu_rdtscp (void) + { + /* + * This extracts the IA32_TSC_AUX into the ecx register. On Linux, + * that value contains a value with the bottom 12 bits being the + * cpu identifier, and the next 10 bits being the node group. + */ + guint aux; + __builtin_ia32_rdtscp (&aux); + return aux & 0xFFF; + } +# define dzl_get_current_cpu() dzl_get_current_cpu_rdtscp() +#elif defined(__linux__) +# define dzl_get_current_cpu() dzl_get_current_cpu_call() +#else +# define dzl_get_current_cpu() 0 +# define DZL_COUNTER_REQUIRES_ATOMIC 1 +#endif + +/** + * DZL_DEFINE_COUNTER: + * @Identifier: The symbol name of the counter + * @Category: A string category for the counter. + * @Name: A string name for the counter. + * @Description: A string description for the counter. + * + * |[ + * DZL_DEFINE_COUNTER (my_counter, "My", "Counter", "My Counter Description"); + * ]| + */ +#define DZL_DEFINE_COUNTER(Identifier, Category, Name, Description) \ + static DzlCounter Identifier##_ctr = { NULL, Category, Name, Description }; \ + static void Identifier##_ctr_init (void) __attribute__((constructor)); \ + static void \ + Identifier##_ctr_init (void) \ + { \ + dzl_counter_arena_register (dzl_counter_arena_get_default(), &Identifier##_ctr); \ + } + +/** + * DZL_COUNTER_INC: + * @Identifier: The identifier of the counter. + * + * Increments the counter @Identifier by 1. + */ +#define DZL_COUNTER_INC(Identifier) DZL_COUNTER_ADD(Identifier, G_GINT64_CONSTANT(1)) + +/** + * DZL_COUNTER_DEC: + * @Identifier: The identifier of the counter. + * + * Decrements the counter @Identifier by 1. + */ +#define DZL_COUNTER_DEC(Identifier) DZL_COUNTER_SUB(Identifier, G_GINT64_CONSTANT(1)) + +/** + * DZL_COUNTER_SUB: + * @Identifier: The identifier of the counter. + * @Count: the amount to subtract. + * + * Subtracts from the counter identified by @Identifier by @Count. + */ +#define DZL_COUNTER_SUB(Identifier, Count) DZL_COUNTER_ADD(Identifier, (-(Count))) + +/** + * DZL_COUNTER_ADD: + * @Identifier: The identifier of the counter. + * @Count: the amount to add to the counter. + * + * Adds @Count to @Identifier. + * + * This operation is not guaranteed to have full correctness. It tries to find + * a happy medium between fast, and accurate. When possible, the %rdtscp + * instruction is used to get a cacheline owned by the executing CPU, to avoid + * collisions. However, this is not guaranteed as the thread could be swapped + * between the calls to %rdtscp and %addq (on 64-bit Intel). + * + * Other platforms have fallbacks which may give different guarantees, such as + * using atomic operations (and therefore, memory barriers). + * + * See #DzlCounter for more information. + */ +#ifdef DZL_COUNTER_REQUIRES_ATOMIC +# define DZL_COUNTER_ADD(Identifier, Count) \ + G_STMT_START { \ + __sync_add_and_fetch ((gint64 *)&Identifier##_ctr.values[0], ((gint64)(Count))); \ + } G_STMT_END +#else +# define DZL_COUNTER_ADD(Identifier, Count) \ + G_STMT_START { \ + Identifier##_ctr.values[dzl_get_current_cpu()].value += ((gint64)(Count)); \ + } G_STMT_END +#endif + +typedef struct _DzlCounter DzlCounter; +typedef struct _DzlCounterArena DzlCounterArena; +typedef struct _DzlCounterValue DzlCounterValue; + +/** + * DzlCounterForeachFunc: + * @counter: the counter. + * @user_data: data supplied to dzl_counter_arena_foreach(). + * + * Function prototype for callbacks provided to dzl_counter_arena_foreach(). + */ +typedef void (*DzlCounterForeachFunc) (DzlCounter *counter, + gpointer user_data); + +struct _DzlCounter +{ + /*< Private >*/ + DzlCounterValue *values; + const gchar *category; + const gchar *name; + const gchar *description; +} __attribute__ ((aligned(8))); + +struct _DzlCounterValue +{ + volatile gint64 value; + gint64 padding [7]; +} __attribute__ ((aligned(8))); + +DZL_AVAILABLE_IN_ALL +GType dzl_counter_arena_get_type (void); +DZL_AVAILABLE_IN_ALL +guint dzl_get_current_cpu_call (void); +DZL_AVAILABLE_IN_ALL +DzlCounterArena *dzl_counter_arena_get_default (void); +DZL_AVAILABLE_IN_ALL +DzlCounterArena *dzl_counter_arena_new_for_pid (GPid pid); +DZL_AVAILABLE_IN_ALL +DzlCounterArena *dzl_counter_arena_ref (DzlCounterArena *arena); +DZL_AVAILABLE_IN_ALL +void dzl_counter_arena_unref (DzlCounterArena *arena); +DZL_AVAILABLE_IN_ALL +void dzl_counter_arena_register (DzlCounterArena *arena, + DzlCounter *counter); +DZL_AVAILABLE_IN_ALL +void dzl_counter_arena_foreach (DzlCounterArena *arena, + DzlCounterForeachFunc func, + gpointer user_data); +DZL_AVAILABLE_IN_ALL +void dzl_counter_reset (DzlCounter *counter); +DZL_AVAILABLE_IN_ALL +gint64 dzl_counter_get (DzlCounter *counter); + +G_END_DECLS + +#endif /* DZL_COUNTER_H */ diff --git a/src/util/dzl-date-time.c b/src/util/dzl-date-time.c new file mode 100644 index 0000000..9120153 --- /dev/null +++ b/src/util/dzl-date-time.c @@ -0,0 +1,116 @@ +/* dzl-date-time.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#include "config.h" + +#include + +#include "dzl-date-time.h" + +/** + * dzl_g_date_time_format_for_display: + * @self: A #GDateTime + * + * Helper function to "humanize" a #GDateTime into a relative time + * relationship string. + * + * Returns: (transfer full): A newly allocated string describing the + * date and time imprecisely such as "Yesterday". + */ +gchar * +dzl_g_date_time_format_for_display (GDateTime *self) +{ + g_autoptr(GDateTime) now = NULL; + GTimeSpan diff; + gint years; + + /* + * TODO: + * + * There is probably a lot more we can do here to be friendly for + * various locales, but this will get us started. + */ + + g_return_val_if_fail (self != NULL, NULL); + + now = g_date_time_new_now_utc (); + diff = g_date_time_difference (now, self) / G_USEC_PER_SEC; + + if (diff < 0) + return g_strdup (""); + else if (diff < (60 * 45)) + return g_strdup (_("Just now")); + else if (diff < (60 * 90)) + return g_strdup (_("An hour ago")); + else if (diff < (60 * 60 * 24 * 2)) + return g_strdup (_("Yesterday")); + else if (diff < (60 * 60 * 24 * 7)) + return g_date_time_format (self, "%A"); + else if (diff < (60 * 60 * 24 * 365)) + return g_date_time_format (self, "%OB"); + else if (diff < (60 * 60 * 24 * 365 * 1.5)) + return g_strdup (_("About a year ago")); + + years = MAX (2, diff / (60 * 60 * 24 * 365)); + + return g_strdup_printf (ngettext ("About %u year ago", "About %u years ago", years), years); +} + +gchar * +dzl_g_time_span_to_label (GTimeSpan span) +{ + gint64 hours; + gint64 minutes; + gint64 seconds; + + span = ABS (span); + + hours = span / G_TIME_SPAN_HOUR; + minutes = (span % G_TIME_SPAN_HOUR) / G_TIME_SPAN_MINUTE; + seconds = (span % G_TIME_SPAN_MINUTE) / G_TIME_SPAN_SECOND; + + g_assert (minutes < 60); + g_assert (seconds < 60); + + if (hours == 0) + return g_strdup_printf ("%02"G_GINT64_FORMAT":%02"G_GINT64_FORMAT, + minutes, seconds); + else + return g_strdup_printf ("%02"G_GINT64_FORMAT":%02"G_GINT64_FORMAT":%02"G_GINT64_FORMAT, + hours, minutes, seconds); +} + +gboolean +dzl_g_time_span_to_label_mapping (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + GTimeSpan span; + + g_assert (G_IS_BINDING (binding)); + g_assert (from_value != NULL); + g_assert (G_VALUE_HOLDS (from_value, G_TYPE_INT64)); + g_assert (to_value != NULL); + g_assert (G_VALUE_HOLDS (to_value, G_TYPE_STRING)); + + span = g_value_get_int64 (from_value); + g_value_take_string (to_value, dzl_g_time_span_to_label (span)); + + return TRUE; +} diff --git a/src/util/dzl-date-time.h b/src/util/dzl-date-time.h new file mode 100644 index 0000000..c608e12 --- /dev/null +++ b/src/util/dzl-date-time.h @@ -0,0 +1,40 @@ +/* dzl-date-time.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_DATE_TIME_H +#define DZL_DATE_TIME_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +gchar *dzl_g_date_time_format_for_display (GDateTime *self); +DZL_AVAILABLE_IN_ALL +gchar *dzl_g_time_span_to_label (GTimeSpan span); +DZL_AVAILABLE_IN_ALL +gboolean dzl_g_time_span_to_label_mapping (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data); + +G_END_DECLS + +#endif /* DZL_DATE_TIME_H */ diff --git a/src/util/dzl-dnd.c b/src/util/dzl-dnd.c new file mode 100644 index 0000000..dfe06cd --- /dev/null +++ b/src/util/dzl-dnd.c @@ -0,0 +1,39 @@ +/* dzl-dnd.c + * + * Copyright (C) 2015 Dimitris Zenios + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-dnd" + +#include "config.h" + +#include "dzl-dnd.h" + +/** + * dzl_dnd_get_uri_list: + * @selection_data: the #GtkSelectionData from drag_data_received + * + * Create a list of valid uri's from a uri-list drop. + * + * Returns: (transfer full): a string array which will hold the uris or + * %NULL if there were no valid uris. g_strfreev should be used when + * the string array is no longer used + */ +gchar ** +dzl_dnd_get_uri_list (GtkSelectionData *selection_data) +{ + return gtk_selection_data_get_uris (selection_data); +} diff --git a/src/util/dzl-dnd.h b/src/util/dzl-dnd.h new file mode 100644 index 0000000..00b5fca --- /dev/null +++ b/src/util/dzl-dnd.h @@ -0,0 +1,33 @@ +/* dzl-dnd.h + * + * Copyright (C) 2015 Dimitris Zenios + * + * 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 . + */ + +#ifndef DZL_DND_H +#define DZL_DND_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +DZL_DEPRECATED_FOR (gtk_selection_data_get_uris) +gchar **dzl_dnd_get_uri_list (GtkSelectionData *selection_data); + +G_END_DECLS + +#endif /* GB_RGBA_H */ diff --git a/src/util/dzl-file-manager.c b/src/util/dzl-file-manager.c new file mode 100644 index 0000000..52052b5 --- /dev/null +++ b/src/util/dzl-file-manager.c @@ -0,0 +1,196 @@ +/* dzl-file-manager.c + * + * Copyright (C) 1995-2017 GIMP Authors + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-file-manager" + +#include "config.h" + +#include + +#if defined(G_OS_WIN32) +/* This is a hack for Windows known directory support. + * DATADIR (autotools-generated constant) is a type defined in objidl.h + * so we must #undef it before including shlobj.h in order to avoid a + * name clash. */ +#undef DATADIR +#include +#include +#endif + +#ifdef PLATFORM_OSX +#include +#endif + +#include "util/dzl-file-manager.h" + +/* Copied from the GIMP */ +gboolean +dzl_file_manager_show (GFile *file, + GError **error) +{ + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + +#if defined(G_OS_WIN32) + + { + gboolean ret; + char *filename; + int n; + LPWSTR w_filename = NULL; + ITEMIDLIST *pidl = NULL; + + ret = FALSE; + + /* Calling this function mutiple times should do no harm, but it is + easier to put this here as it needs linking against ole32. */ + CoInitialize (NULL); + + filename = g_file_get_path (file); + if (!filename) + { + g_set_error_literal (error, G_FILE_ERROR, 0, + _("File path is NULL")); + goto out; + } + + n = MultiByteToWideChar (CP_UTF8, MB_ERR_INVALID_CHARS, + filename, -1, NULL, 0); + if (n == 0) + { + g_set_error_literal (error, G_FILE_ERROR, 0, + _("Error converting UTF-8 filename to wide char")); + goto out; + } + + w_filename = g_malloc_n (n + 1, sizeof (wchar_t)); + n = MultiByteToWideChar (CP_UTF8, MB_ERR_INVALID_CHARS, + filename, -1, + w_filename, (n + 1) * sizeof (wchar_t)); + if (n == 0) + { + g_set_error_literal (error, G_FILE_ERROR, 0, + _("Error converting UTF-8 filename to wide char")); + goto out; + } + + pidl = ILCreateFromPathW (w_filename); + if (!pidl) + { + g_set_error_literal (error, G_FILE_ERROR, 0, + _("ILCreateFromPath() failed")); + goto out; + } + + SHOpenFolderAndSelectItems (pidl, 0, NULL, 0); + ret = TRUE; + + out: + if (pidl) + ILFree (pidl); + g_free (w_filename); + g_free (filename); + + return ret; + } + +#elif defined(PLATFORM_OSX) + + { + gchar *uri; + NSString *filename; + NSURL *url; + gboolean retval = TRUE; + + uri = g_file_get_uri (file); + filename = [NSString stringWithUTF8String:uri]; + + url = [NSURL URLWithString:filename]; + if (url) + { + NSArray *url_array = [NSArray arrayWithObject:url]; + + [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:url_array]; + } + else + { + g_set_error (error, G_FILE_ERROR, 0, + _("Cannot convert “%s” into a valid NSURL."), uri); + retval = FALSE; + } + + g_free (uri); + + return retval; + } + +#else /* UNIX */ + + { + GDBusProxy *proxy; + GVariant *retval; + GVariantBuilder *builder; + gchar *uri; + + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.FileManager1", + "/org/freedesktop/FileManager1", + "org.freedesktop.FileManager1", + NULL, error); + + if (!proxy) + { + g_prefix_error (error, + _("Connecting to org.freedesktop.FileManager1 failed: ")); + return FALSE; + } + + uri = g_file_get_uri (file); + + builder = g_variant_builder_new (G_VARIANT_TYPE ("as")); + g_variant_builder_add (builder, "s", uri); + + g_free (uri); + + retval = g_dbus_proxy_call_sync (proxy, + "ShowItems", + g_variant_new ("(ass)", + builder, + ""), + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, error); + + g_variant_builder_unref (builder); + g_object_unref (proxy); + + if (!retval) + { + g_prefix_error (error, _("Calling ShowItems failed: ")); + return FALSE; + } + + g_variant_unref (retval); + + return TRUE; + } + +#endif +} diff --git a/src/util/dzl-file-manager.h b/src/util/dzl-file-manager.h new file mode 100644 index 0000000..c8da6b5 --- /dev/null +++ b/src/util/dzl-file-manager.h @@ -0,0 +1,34 @@ +/* dzl-file-manager.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_FILE_MANAGER_H +#define DZL_FILE_MANAGER_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +gboolean dzl_file_manager_show (GFile *file, + GError **error); + +G_END_DECLS + +#endif /* DZL_FILE_MANAGER_H */ diff --git a/src/util/dzl-gdk.c b/src/util/dzl-gdk.c new file mode 100644 index 0000000..0a4062a --- /dev/null +++ b/src/util/dzl-gdk.c @@ -0,0 +1,161 @@ +/* dzl-gdk.c + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-gdk" + +#include "config.h" + +#include + +#include "dzl-gdk.h" + +/** + * dzl_gdk_synthesize_event_key: (skip) + * + * Returns: (transfer full): A #GdkEventKey + */ +GdkEventKey * +dzl_gdk_synthesize_event_key (GdkWindow *window, + gunichar ch) +{ + GdkDisplay *display; + GdkKeymap *keymap; + GdkDevice *device; + GdkSeat *seat; + GdkEvent *ev; + GdkKeymapKey *keys = NULL; + gint n_keys = 0; + gchar str[8] = { 0 }; + + g_return_val_if_fail (window != NULL, NULL); + g_return_val_if_fail (GDK_IS_WINDOW (window), NULL); + + g_unichar_to_utf8 (ch, str); + + ev = gdk_event_new (GDK_KEY_PRESS); + ev->key.window = g_object_ref (window); + ev->key.send_event = TRUE; + ev->key.time = gtk_get_current_event_time (); + ev->key.state = 0; + ev->key.hardware_keycode = 0; + ev->key.group = 0; + ev->key.is_modifier = 0; + + switch (ch) + { + case '\n': + ev->key.keyval = GDK_KEY_Return; + ev->key.string = g_strdup ("\n"); + ev->key.length = 1; + break; + + case '\e': + ev->key.keyval = GDK_KEY_Escape; + ev->key.string = g_strdup (""); + ev->key.length = 0; + break; + + default: + ev->key.keyval = gdk_unicode_to_keyval (ch); + ev->key.length = strlen (str); + ev->key.string = g_strdup (str); + break; + } + + display = gdk_window_get_display (window); + keymap = gdk_keymap_get_for_display (display); + gdk_keymap_get_entries_for_keyval (keymap, + ev->key.keyval, + &keys, + &n_keys); + + if (n_keys > 0) + { + ev->key.hardware_keycode = keys [0].keycode; + ev->key.group = keys [0].group; + if (keys [0].level == 1) + ev->key.state |= GDK_SHIFT_MASK; + g_free (keys); + } + + seat = gdk_display_get_default_seat (display); + device = gdk_seat_get_keyboard (seat); + gdk_event_set_device (ev, device); + + return &ev->key; +} + +/** + * dzl_gdk_synthesize_event_keyval: (skip) + * + * Returns: (transfer full): A #GdkEventKey + */ +GdkEventKey * +dzl_gdk_synthesize_event_keyval (GdkWindow *window, + guint keyval) +{ + GdkDisplay *display; + GdkDevice *device; + GdkKeymap *keymap; + GdkEvent *ev; + GdkSeat *seat; + GdkKeymapKey *keys = NULL; + gint n_keys = 0; + gchar str[8] = { 0 }; + gunichar ch; + + g_return_val_if_fail (window != NULL, NULL); + g_return_val_if_fail (GDK_IS_WINDOW (window), NULL); + + ch = gdk_keyval_to_unicode (keyval); + g_unichar_to_utf8 (ch, str); + + ev = gdk_event_new (GDK_KEY_PRESS); + ev->key.window = g_object_ref (window); + ev->key.send_event = TRUE; + ev->key.time = gtk_get_current_event_time (); + ev->key.state = 0; + ev->key.hardware_keycode = 0; + ev->key.group = 0; + ev->key.is_modifier = 0; + ev->key.keyval = keyval; + ev->key.string = g_strdup (str); + ev->key.length = strlen (str); + + display = gdk_window_get_display (window); + keymap = gdk_keymap_get_for_display (display); + gdk_keymap_get_entries_for_keyval (keymap, + ev->key.keyval, + &keys, + &n_keys); + + if (n_keys > 0) + { + ev->key.hardware_keycode = keys [0].keycode; + ev->key.group = keys [0].group; + if (keys [0].level == 1) + ev->key.state |= GDK_SHIFT_MASK; + g_free (keys); + } + + seat = gdk_display_get_default_seat (display); + device = gdk_seat_get_keyboard (seat); + gdk_event_set_device (ev, device); + + return &ev->key; +} diff --git a/src/util/dzl-gdk.h b/src/util/dzl-gdk.h new file mode 100644 index 0000000..24dfcd7 --- /dev/null +++ b/src/util/dzl-gdk.h @@ -0,0 +1,37 @@ +/* dzl-gdk.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_GDK_H +#define DZL_GDK_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +GdkEventKey *dzl_gdk_synthesize_event_key (GdkWindow *window, + gunichar ch); +DZL_AVAILABLE_IN_ALL +GdkEventKey *dzl_gdk_synthesize_event_keyval (GdkWindow *window, + guint keyval); + +G_END_DECLS + +#endif /* DZL_GDK_H */ diff --git a/src/util/dzl-gtk.c b/src/util/dzl-gtk.c new file mode 100644 index 0000000..95ac321 --- /dev/null +++ b/src/util/dzl-gtk.c @@ -0,0 +1,705 @@ +/* dzl-gtk.c + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-gtk" + +#include "config.h" + +#include "animation/dzl-animation.h" +#include "util/dzl-gtk.h" + +gboolean +dzl_gtk_widget_action (GtkWidget *widget, + const gchar *prefix, + const gchar *action_name, + GVariant *parameter) +{ + GtkWidget *toplevel; + GApplication *app; + GActionGroup *group = NULL; + + g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE); + g_return_val_if_fail (prefix, FALSE); + g_return_val_if_fail (action_name, FALSE); + + app = g_application_get_default (); + toplevel = gtk_widget_get_toplevel (widget); + + while ((group == NULL) && (widget != NULL)) + { + group = gtk_widget_get_action_group (widget, prefix); + + if G_UNLIKELY (GTK_IS_POPOVER (widget)) + { + GtkWidget *relative_to; + + relative_to = gtk_popover_get_relative_to (GTK_POPOVER (widget)); + + if (relative_to != NULL) + widget = relative_to; + else + widget = gtk_widget_get_parent (widget); + } + else + { + widget = gtk_widget_get_parent (widget); + } + } + + if (!group && g_str_equal (prefix, "win") && G_IS_ACTION_GROUP (toplevel)) + group = G_ACTION_GROUP (toplevel); + + if (!group && g_str_equal (prefix, "app") && G_IS_ACTION_GROUP (app)) + group = G_ACTION_GROUP (app); + + if (group && g_action_group_has_action (group, action_name)) + { + g_action_group_activate_action (group, action_name, parameter); + return TRUE; + } + + if (parameter && g_variant_is_floating (parameter)) + { + parameter = g_variant_ref_sink (parameter); + g_variant_unref (parameter); + } + + g_warning ("Failed to locate action %s.%s", prefix, action_name); + + return FALSE; +} + +gboolean +dzl_gtk_widget_action_with_string (GtkWidget *widget, + const gchar *group, + const gchar *name, + const gchar *param) +{ + g_autoptr(GVariant) variant = NULL; + gboolean ret; + + g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE); + g_return_val_if_fail (group != NULL, FALSE); + g_return_val_if_fail (name != NULL, FALSE); + + if (param == NULL) + param = ""; + + if (*param != 0) + { + g_autoptr(GError) error = NULL; + + variant = g_variant_parse (NULL, param, NULL, NULL, &error); + + if (variant == NULL) + { + g_warning ("can't parse keybinding parameters \"%s\": %s", + param, error->message); + return FALSE; + } + } + + ret = dzl_gtk_widget_action (widget, group, name, variant); + + return ret; +} + +static void +show_callback (gpointer data) +{ + g_object_set_data (data, "DZL_FADE_ANIMATION", NULL); + g_object_unref (data); +} + +static void +hide_callback (gpointer data) +{ + GtkWidget *widget = data; + + g_object_set_data (data, "DZL_FADE_ANIMATION", NULL); + gtk_widget_hide (widget); + gtk_widget_set_opacity (widget, 1.0); + g_object_unref (widget); +} + +void +dzl_gtk_widget_hide_with_fade (GtkWidget *widget) +{ + GdkFrameClock *frame_clock; + DzlAnimation *anim; + + g_return_if_fail (GTK_IS_WIDGET (widget)); + + if (gtk_widget_get_visible (widget)) + { + anim = g_object_get_data (G_OBJECT (widget), "DZL_FADE_ANIMATION"); + if (anim != NULL) + dzl_animation_stop (anim); + + frame_clock = gtk_widget_get_frame_clock (widget); + anim = dzl_object_animate_full (widget, + DZL_ANIMATION_LINEAR, + 1000, + frame_clock, + hide_callback, + g_object_ref (widget), + "opacity", 0.0, + NULL); + g_object_set_data_full (G_OBJECT (widget), "DZL_FADE_ANIMATION", + g_object_ref (anim), g_object_unref); + } +} + +void +dzl_gtk_widget_show_with_fade (GtkWidget *widget) +{ + GdkFrameClock *frame_clock; + DzlAnimation *anim; + + g_return_if_fail (GTK_IS_WIDGET (widget)); + + if (!gtk_widget_get_visible (widget)) + { + anim = g_object_get_data (G_OBJECT (widget), "DZL_FADE_ANIMATION"); + if (anim != NULL) + dzl_animation_stop (anim); + + frame_clock = gtk_widget_get_frame_clock (widget); + gtk_widget_set_opacity (widget, 0.0); + gtk_widget_show (widget); + anim = dzl_object_animate_full (widget, + DZL_ANIMATION_LINEAR, + 500, + frame_clock, + show_callback, + g_object_ref (widget), + "opacity", 1.0, + NULL); + g_object_set_data_full (G_OBJECT (widget), "DZL_FADE_ANIMATION", + g_object_ref (anim), g_object_unref); + } +} + +static void +dzl_gtk_widget_find_child_typed_cb (GtkWidget *widget, + gpointer user_data) +{ + struct { + gpointer ret; + GType type; + } *state = user_data; + + if (state->ret != NULL) + return; + + if (g_type_is_a (G_OBJECT_TYPE (widget), state->type)) + { + state->ret = widget; + } + else if (GTK_IS_CONTAINER (widget)) + { + gtk_container_foreach (GTK_CONTAINER (widget), + dzl_gtk_widget_find_child_typed_cb, + state); + } +} + +/** + * dzl_gtk_widget_find_child_typed: + * + * Tries to locate a widget in a hierarchy given it's #GType. + * + * There is not an efficient implementation of this method, so use it + * only when the hierarchy of widgets is small. + * + * Returns: (transfer none) (type Gtk.Widget) (nullable): A widget or %NULL + */ +gpointer +dzl_gtk_widget_find_child_typed (GtkWidget *widget, + GType child_type) +{ + struct { + gpointer ret; + GType type; + } state; + + g_return_val_if_fail (GTK_IS_CONTAINER (widget), NULL); + g_return_val_if_fail (g_type_is_a (child_type, GTK_TYPE_WIDGET), NULL); + + state.ret = NULL; + state.type = child_type; + + gtk_container_foreach (GTK_CONTAINER (widget), + dzl_gtk_widget_find_child_typed_cb, + &state); + + return state.ret; +} + +/** + * dzl_gtk_text_buffer_remove_tag: + * + * Like gtk_text_buffer_remove_tag() but allows specifying that the tags + * should be removed one at a time to avoid over-damaging the views + * displaying @buffer. + */ +void +dzl_gtk_text_buffer_remove_tag (GtkTextBuffer *buffer, + GtkTextTag *tag, + const GtkTextIter *start, + const GtkTextIter *end, + gboolean minimal_damage) +{ + GtkTextIter tag_begin; + GtkTextIter tag_end; + + g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer)); + g_return_if_fail (GTK_IS_TEXT_TAG (tag)); + g_return_if_fail (start != NULL); + g_return_if_fail (end != NULL); + + if (!minimal_damage) + { + gtk_text_buffer_remove_tag (buffer, tag, start, end); + return; + } + + tag_begin = *start; + + if (!gtk_text_iter_starts_tag (&tag_begin, tag)) + { + if (!gtk_text_iter_forward_to_tag_toggle (&tag_begin, tag)) + return; + } + + while (gtk_text_iter_starts_tag (&tag_begin, tag) && + gtk_text_iter_compare (&tag_begin, end) < 0) + { + gint count = 1; + + tag_end = tag_begin; + + /* + * We might have found the start of another tag embedded + * inside this tag. So keep scanning forward until we have + * reached the right number of end tags. + */ + + while (gtk_text_iter_forward_to_tag_toggle (&tag_end, tag)) + { + if (gtk_text_iter_starts_tag (&tag_end, tag)) + count++; + else if (gtk_text_iter_ends_tag (&tag_end, tag)) + count--; + + if (count == 0) + break; + } + + if (gtk_text_iter_ends_tag (&tag_end, tag)) + gtk_text_buffer_remove_tag (buffer, tag, &tag_begin, &tag_end); + + tag_begin = tag_end; + + /* + * Move to the next start tag. It's possible to have an overlapped + * end tag, which would be non-ideal, but possible. + */ + if (!gtk_text_iter_starts_tag (&tag_begin, tag)) + { + while (gtk_text_iter_forward_to_tag_toggle (&tag_begin, tag)) + { + if (gtk_text_iter_starts_tag (&tag_begin, tag)) + break; + } + } + } +} + +void +dzl_gtk_widget_add_style_class (GtkWidget *widget, + const gchar *class_name) +{ + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (class_name != NULL); + + gtk_style_context_add_class (gtk_widget_get_style_context (widget), class_name); +} + +void +dzl_gtk_widget_remove_style_class (GtkWidget *widget, + const gchar *class_name) +{ + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (class_name != NULL); + + gtk_style_context_remove_class (gtk_widget_get_style_context (widget), class_name); +} + +void +dzl_gtk_widget_action_set (GtkWidget *widget, + const gchar *group, + const gchar *name, + const gchar *first_property, + ...) +{ + GAction *action = NULL; + va_list args; + + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (group != NULL); + g_return_if_fail (name != NULL); + g_return_if_fail (first_property != NULL); + + for (; widget; widget = gtk_widget_get_parent (widget)) + { + GActionGroup *actions = gtk_widget_get_action_group (widget, group); + + if (G_IS_ACTION_MAP (actions)) + { + action = g_action_map_lookup_action (G_ACTION_MAP (actions), name); + + if (action != NULL) + break; + } + } + + if (action == NULL) + { + g_warning ("Failed to locate action %s.%s", group, name); + return; + } + + va_start (args, first_property); + g_object_set_valist (G_OBJECT (action), first_property, args); + va_end (args); +} + +/** + * dzl_gtk_widget_mux_action_groups: + * @widget: a #GtkWidget + * @from_widget: A #GtkWidget containing the groups to copy + * @mux_key: (nullable): a unique key to represent the muxing + * + * This function will find all of the actions on @from_widget in various + * groups and add them to @widget. As this just copies the action groups + * over, note that it does not allow for muxing items within the same + * group. + * + * You should specify a key for @mux_key so that if the same mux key is + * seen again, the previous muxings will be removed. + */ +void +dzl_gtk_widget_mux_action_groups (GtkWidget *widget, + GtkWidget *from_widget, + const gchar *mux_key) +{ + const gchar * const *old_prefixes = NULL; + g_auto(GStrv) prefixes = NULL; + + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (!from_widget || GTK_IS_WIDGET (from_widget)); + g_return_if_fail (widget != from_widget); + + if (mux_key == NULL) + mux_key = "DZL_GTK_MUX_ACTIONS"; + + /* + * First check to see if there are any old action groups that + * were muxed for which we need to unmux. + */ + + old_prefixes = g_object_get_data (G_OBJECT (widget), mux_key); + + if (old_prefixes != NULL) + { + for (guint i = 0; old_prefixes [i]; i++) + { + if (g_str_equal (old_prefixes [i], "win") || g_str_equal (old_prefixes [i], "app")) + continue; + + gtk_widget_insert_action_group (widget, old_prefixes [i], NULL); + } + } + + /* + * Now, if there is a from_widget to mux, get all of their action + * groups and mux them across to our target widget. + */ + + if (from_widget != NULL) + { + g_autofree const gchar **tmp = gtk_widget_list_action_prefixes (from_widget); + + if (tmp != NULL) + { + prefixes = g_strdupv ((gchar **)tmp); + + for (guint i = 0; prefixes [i]; i++) + { + GActionGroup *group = gtk_widget_get_action_group (from_widget, prefixes [i]); + + if (g_str_equal (prefixes [i], "win") || g_str_equal (prefixes [i], "app")) + continue; + + if G_UNLIKELY (group == NULL) + continue; + + gtk_widget_insert_action_group (widget, prefixes[i], group); + } + } + } + + /* Store the set of muxed prefixes so that we can unmux them later. */ + g_object_set_data_full (G_OBJECT (widget), + mux_key, + g_steal_pointer (&prefixes), + (GDestroyNotify) g_strfreev); +} + +static gboolean +list_store_iter_middle (GtkListStore *store, + const GtkTreeIter *begin, + const GtkTreeIter *end, + GtkTreeIter *middle) +{ + g_assert (store != NULL); + g_assert (begin != NULL); + g_assert (end != NULL); + g_assert (middle != NULL); + g_assert (middle->stamp == begin->stamp); + g_assert (middle->stamp == end->stamp); + + /* + * middle MUST ALREADY BE VALID as it saves us some copying + * as well as just makes things easier when binary searching. + */ + + middle->user_data = g_sequence_range_get_midpoint (begin->user_data, end->user_data); + + if (g_sequence_iter_is_end (middle->user_data)) + { + middle->stamp = 0; + return FALSE; + } + + return TRUE; +} + +static inline gboolean +list_store_iter_equal (const GtkTreeIter *a, + const GtkTreeIter *b) +{ + return a->user_data == b->user_data; +} + +/** + * dzl_gtk_list_store_insert_sorted: + * @store: A #GtkListStore + * @iter: (out): A location for a #GtkTextIter + * @key: A key to compare to when binary searching + * @compare_column: the column containing the data to compare + * @compare_func: (scope call) (closure compare_data): A callback to compare + * @compare_data: data for @compare_func + * + * This function will binary search the contents of @store looking for the + * location to insert a new row. + * + * @compare_column must be the index of a column that is a %G_TYPE_POINTER, + * %G_TYPE_BOXED or %G_TYPE_OBJECT based column. + * + * @compare_func will be called with @key as the first parameter and the + * value from the #GtkListStore row as the second parameter. The third and + * final parameter is @compare_data. + * + * Since: 3.26 + */ +void +dzl_gtk_list_store_insert_sorted (GtkListStore *store, + GtkTreeIter *iter, + gconstpointer key, + guint compare_column, + GCompareDataFunc compare_func, + gpointer compare_data) +{ + GValue value = G_VALUE_INIT; + gpointer (*get_func) (const GValue *) = NULL; + GtkTreeModel *model = (GtkTreeModel *)store; + GtkTreeIter begin; + GtkTreeIter end; + GtkTreeIter middle; + guint n_children; + gint cmpval = 0; + GType type; + + g_return_if_fail (GTK_IS_LIST_STORE (store)); + g_return_if_fail (GTK_IS_LIST_STORE (model)); + g_return_if_fail (iter != NULL); + g_return_if_fail (compare_column < gtk_tree_model_get_n_columns (GTK_TREE_MODEL (store))); + g_return_if_fail (compare_func != NULL); + + type = gtk_tree_model_get_column_type (GTK_TREE_MODEL (store), compare_column); + + if (g_type_is_a (type, G_TYPE_POINTER)) + get_func = g_value_get_pointer; + else if (g_type_is_a (type, G_TYPE_BOXED)) + get_func = g_value_get_boxed; + else if (g_type_is_a (type, G_TYPE_OBJECT)) + get_func = g_value_get_object; + else + { + g_warning ("%s() only supports pointer, boxed, or object columns", + G_STRFUNC); + gtk_list_store_append (store, iter); + return; + } + + /* Try to get the first iter instead of calling n_children to + * avoid walking the GSequence all the way to the right. If this + * matches, we know there are some children. + */ + if (!gtk_tree_model_get_iter_first (model, &begin)) + { + gtk_list_store_append (store, iter); + return; + } + + n_children = gtk_tree_model_iter_n_children (model, NULL); + if (!gtk_tree_model_iter_nth_child (model, &end, NULL, n_children - 1)) + g_assert_not_reached (); + + middle = begin; + + while (list_store_iter_middle (store, &begin, &end, &middle)) + { + gtk_tree_model_get_value (model, &middle, compare_column, &value); + cmpval = compare_func (key, get_func (&value), compare_data); + g_value_unset (&value); + + if (cmpval == 0 || list_store_iter_equal (&begin, &end)) + break; + + if (cmpval < 0) + { + end = middle; + + if (!list_store_iter_equal (&begin, &end) && + !gtk_tree_model_iter_previous (model, &end)) + break; + } + else if (cmpval > 0) + { + begin = middle; + + if (!list_store_iter_equal (&begin, &end) && + !gtk_tree_model_iter_next (model, &begin)) + break; + } + else + g_assert_not_reached (); + } + + if (cmpval < 0) + gtk_list_store_insert_before (store, iter, &middle); + else + gtk_list_store_insert_after (store, iter, &middle); +} + +static GtkWidget * +get_parent_or_relative (GtkWidget *widget) +{ + GtkWidget *parent = NULL; + + g_assert (GTK_IS_WIDGET (widget)); + + if (GTK_IS_POPOVER (widget)) + parent = gtk_popover_get_relative_to (GTK_POPOVER (widget)); + else if (GTK_IS_WINDOW (widget)) + parent = (GtkWidget *)gtk_window_get_transient_for (GTK_WINDOW (widget)); + else if (GTK_IS_MENU (widget)) + parent = gtk_menu_get_attach_widget (GTK_MENU (widget)); + + if (parent == NULL) + parent = gtk_widget_get_parent (widget); + + return parent; +} + +/** + * dzl_gtk_widget_is_ancestor_or_relative: + * @widget: a #GtkWidget + * @ancestor: a #GtkWidget that might be an ancestor + * + * This function is like gtk_widget_is_ancestor() except that it checks + * various relative widgets that are not in the direct hierarchy of + * widgets. That includes #GtkMenu:attach-widget, + * #GtkPopover:relative-to, and #GtkWindow:transient-for. + * + * Returns: %TRUE if @ancestor is an ancestor or relative for @widget. + * + * Since: 3.26 + */ +gboolean +dzl_gtk_widget_is_ancestor_or_relative (GtkWidget *widget, + GtkWidget *ancestor) +{ + g_return_val_if_fail (!widget || GTK_IS_WIDGET (widget), FALSE); + g_return_val_if_fail (!ancestor || GTK_IS_WIDGET (ancestor), FALSE); + + if (widget == NULL || ancestor == NULL) + return FALSE; + + do + { + if (widget == ancestor) + return TRUE; + } + while (NULL != (widget = get_parent_or_relative (widget))); + + return FALSE; +} + +/** + * dzl_gtk_widget_get_relative: + * @widget: a #GtkWidget + * @relative_type: the type of widget to locate + * + * This is similar to gtk_widget_get_ancestor(), but looks for relatives + * via properties such as #GtkPopover:relative-to and others. + * + * Returns: (transfer none) (nullable): A #GtkWidget or %NULL. + */ +GtkWidget * +dzl_gtk_widget_get_relative (GtkWidget *widget, + GType relative_type) +{ + g_return_val_if_fail (!widget || GTK_IS_WIDGET (widget), NULL); + g_return_val_if_fail (g_type_is_a (relative_type, GTK_TYPE_WIDGET), NULL); + + if (widget == NULL) + return FALSE; + + do + { + if (g_type_is_a (G_OBJECT_TYPE (widget), relative_type)) + return widget; + } + while (NULL != (widget = get_parent_or_relative (widget))); + + return NULL; +} diff --git a/src/util/dzl-gtk.h b/src/util/dzl-gtk.h new file mode 100644 index 0000000..cb21b4c --- /dev/null +++ b/src/util/dzl-gtk.h @@ -0,0 +1,83 @@ +/* dzl-gtk.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_GTK_H +#define DZL_GTK_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +void dzl_gtk_widget_action_set (GtkWidget *widget, + const gchar *group, + const gchar *name, + const gchar *first_property, + ...) G_GNUC_NULL_TERMINATED; +DZL_AVAILABLE_IN_ALL +gboolean dzl_gtk_widget_action (GtkWidget *widget, + const gchar *group, + const gchar *name, + GVariant *param); +DZL_AVAILABLE_IN_ALL +gboolean dzl_gtk_widget_action_with_string (GtkWidget *widget, + const gchar *group, + const gchar *name, + const gchar *param); +DZL_AVAILABLE_IN_ALL +void dzl_gtk_widget_mux_action_groups (GtkWidget *widget, + GtkWidget *from_widget, + const gchar *mux_key); +DZL_AVAILABLE_IN_ALL +void dzl_gtk_widget_hide_with_fade (GtkWidget *widget); +DZL_AVAILABLE_IN_ALL +void dzl_gtk_widget_show_with_fade (GtkWidget *widget); +DZL_AVAILABLE_IN_ALL +void dzl_gtk_widget_add_style_class (GtkWidget *widget, + const gchar *class_name); +DZL_AVAILABLE_IN_ALL +void dzl_gtk_widget_remove_style_class (GtkWidget *widget, + const gchar *class_name); +DZL_AVAILABLE_IN_ALL +gpointer dzl_gtk_widget_find_child_typed (GtkWidget *widget, + GType type); +DZL_AVAILABLE_IN_ALL +void dzl_gtk_text_buffer_remove_tag (GtkTextBuffer *buffer, + GtkTextTag *tag, + const GtkTextIter *start, + const GtkTextIter *end, + gboolean minimal_damage); +DZL_AVAILABLE_IN_ALL +void dzl_gtk_list_store_insert_sorted (GtkListStore *store, + GtkTreeIter *iter, + gconstpointer key, + guint compare_column, + GCompareDataFunc compare_func, + gpointer compare_data); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_gtk_widget_get_relative (GtkWidget *widget, + GType relative_type); +DZL_AVAILABLE_IN_ALL +gboolean dzl_gtk_widget_is_ancestor_or_relative (GtkWidget *widget, + GtkWidget *ancestor); + +G_END_DECLS + +#endif /* DZL_GTK_H */ diff --git a/src/util/dzl-heap.c b/src/util/dzl-heap.c new file mode 100644 index 0000000..c4dd472 --- /dev/null +++ b/src/util/dzl-heap.c @@ -0,0 +1,389 @@ +/* dzl-heap.c + * + * Copyright (C) 2014 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-heap" + +#include "config.h" + +#include + +#include "dzl-heap.h" + +/** + * SECTION:dzlheap + * @title: Heaps + * @short_description: efficient priority queues using min/max heaps + * + * Heaps are similar to a partially sorted tree but implemented as an + * array. They allow for efficient O(1) lookup of the highest priority + * item as it will always be the first item of the array. + * + * To create a new heap use dzl_heap_new(). + * + * To add items to the heap, use dzl_heap_insert_val() or + * dzl_heap_insert_vals() to insert in bulk. + * + * To access an item in the heap, use dzl_heap_index(). + * + * To remove an arbitrary item from the heap, use dzl_heap_extract_index(). + * + * To remove the highest priority item in the heap, use dzl_heap_extract(). + * + * To free a heap, use dzl_heap_unref(). + * + * Here is an example that stores integers in a #DzlHeap: + * |[ + * static int + * cmpint (gconstpointer a, + * gconstpointer b) + * { + * return *(const gint *)a - *(const gint *)b; + * } + * + * int + * main (gint argc, + * gchar *argv[]) + * { + * DzlHeap *heap; + * gint i; + * gint v; + * + * heap = dzl_heap_new (sizeof (gint), cmpint); + * + * for (i = 0; i < 10000; i++) + * dzl_heap_insert_val (heap, i); + * for (i = 0; i < 10000; i++) + * dzl_heap_extract (heap, &v); + * + * dzl_heap_unref (heap); + * } + * ]| + */ + +#define MIN_HEAP_SIZE 16 + +/* + * Based upon Mastering Algorithms in C by Kyle Loudon. + * Section 10 - Heaps and Priority Queues. + */ + +G_DEFINE_BOXED_TYPE (DzlHeap, dzl_heap, dzl_heap_ref, dzl_heap_unref) + +typedef struct _DzlHeapReal DzlHeapReal; + +struct _DzlHeapReal +{ + gchar *data; + gssize len; + volatile gint ref_count; + guint element_size; + gsize allocated_len; + GCompareFunc compare; + gchar tmp[0]; +}; + +#define heap_parent(npos) (((npos)-1)/2) +#define heap_left(npos) (((npos)*2)+1) +#define heap_right(npos) (((npos)*2)+2) +#define heap_index(h,i) ((h)->data + (i * (h)->element_size)) +#define heap_compare(h,a,b) ((h)->compare(heap_index(h,a), heap_index(h,b))) +#define heap_swap(h,a,b) \ + G_STMT_START { \ + memcpy ((h)->tmp, heap_index (h, a), (h)->element_size); \ + memcpy (heap_index (h, a), heap_index (h, b), (h)->element_size); \ + memcpy (heap_index (h, b), (h)->tmp, (h)->element_size); \ + } G_STMT_END + +/** + * dzl_heap_new: + * @element_size: the size of each element in the heap + * @compare_func: (scope async): a function to compare to elements + * + * Creates a new #DzlHeap. A heap is a tree-like structure stored in + * an array that is not fully sorted, but head is guaranteed to be either + * the max, or min value based on @compare_func. This is also known as + * a priority queue. + * + * Returns: (transfer full): A newly allocated #DzlHeap + */ +DzlHeap * +dzl_heap_new (guint element_size, + GCompareFunc compare_func) +{ + DzlHeapReal *real; + + g_return_val_if_fail (element_size, NULL); + g_return_val_if_fail (compare_func, NULL); + + real = g_malloc_n (1, sizeof (DzlHeapReal) + element_size); + real->data = NULL; + real->len = 0; + real->ref_count = 1; + real->element_size = element_size; + real->allocated_len = 0; + real->compare = compare_func; + + return (DzlHeap *)real; +} + +/** + * dzl_heap_ref: + * @heap: An #DzlHeap + * + * Increments the reference count of @heap by one. + * + * Returns: (transfer full): @heap + */ +DzlHeap * +dzl_heap_ref (DzlHeap *heap) +{ + DzlHeapReal *real = (DzlHeapReal *)heap; + + g_return_val_if_fail (heap, NULL); + g_return_val_if_fail (real->ref_count, NULL); + + g_atomic_int_inc (&real->ref_count); + + return heap; +} + +static void +dzl_heap_real_free (DzlHeapReal *real) +{ + g_assert (real); + g_assert_cmpint (real->ref_count, ==, 0); + + g_free (real->data); + g_free (real); +} + +/** + * dzl_heap_unref: + * @heap: (transfer full): An #DzlHeap + * + * Decrements the reference count of @heap by one, freeing the structure + * when the reference count reaches zero. + */ +void +dzl_heap_unref (DzlHeap *heap) +{ + DzlHeapReal *real = (DzlHeapReal *)heap; + + g_return_if_fail (heap); + g_return_if_fail (real->ref_count); + + if (g_atomic_int_dec_and_test (&real->ref_count)) + dzl_heap_real_free (real); +} + +static void +dzl_heap_real_grow (DzlHeapReal *real) +{ + g_assert (real); + g_assert_cmpint (real->allocated_len, <, G_MAXSIZE / 2); + + real->allocated_len = MAX (MIN_HEAP_SIZE, (real->allocated_len * 2)); + real->data = g_realloc_n (real->data, + real->allocated_len, + real->element_size); +} + +static void +dzl_heap_real_shrink (DzlHeapReal *real) +{ + g_assert (real); + g_assert ((real->allocated_len / 2) >= (gsize)real->len); + + real->allocated_len = MAX (MIN_HEAP_SIZE, real->allocated_len / 2); + real->data = g_realloc_n (real->data, + real->allocated_len, + real->element_size); +} + +static void +dzl_heap_real_insert_val (DzlHeapReal *real, + gconstpointer data) +{ + gint ipos; + gint ppos; + + g_assert (real); + g_assert (data); + + if (G_UNLIKELY ((gsize)real->len == real->allocated_len)) + dzl_heap_real_grow (real); + + memcpy (real->data + (real->element_size * real->len), + data, + real->element_size); + + ipos = real->len; + ppos = heap_parent (ipos); + + while ((ipos > 0) && (heap_compare (real, ppos, ipos) < 0)) + { + heap_swap (real, ppos, ipos); + ipos = ppos; + ppos = heap_parent (ipos); + } + + real->len++; +} + +void +dzl_heap_insert_vals (DzlHeap *heap, + gconstpointer data, + guint len) +{ + DzlHeapReal *real = (DzlHeapReal *)heap; + const gchar *ptr = data; + guint i; + + g_return_if_fail (heap); + g_return_if_fail (data); + g_return_if_fail (len); + g_return_if_fail ((G_MAXSSIZE - len) > real->len); + + for (i = 0; i < len; i++, ptr += real->element_size) + dzl_heap_real_insert_val (real, ptr); +} + +gboolean +dzl_heap_extract (DzlHeap *heap, + gpointer result) +{ + DzlHeapReal *real = (DzlHeapReal *)heap; + gint ipos; + gint lpos; + gint rpos; + gint mpos; + + g_return_val_if_fail (heap, FALSE); + + if (real->len == 0) + return FALSE; + + if (result) + memcpy (result, heap_index (real, 0), real->element_size); + + if (--real->len > 0) + { + memmove (real->data, + heap_index (real, real->len), + real->element_size); + + ipos = 0; + + while (TRUE) + { + lpos = heap_left (ipos); + rpos = heap_right (ipos); + + if ((lpos < real->len) && (heap_compare (real, lpos, ipos) > 0)) + mpos = lpos; + else + mpos = ipos; + + if ((rpos < real->len) && (heap_compare (real, rpos, mpos) > 0)) + mpos = rpos; + + if (mpos == ipos) + break; + + heap_swap (real, mpos, ipos); + + ipos = mpos; + } + } + + if ((real->len > MIN_HEAP_SIZE) && (real->allocated_len / 2) >= (gsize)real->len) + dzl_heap_real_shrink (real); + + return TRUE; +} + +gboolean +dzl_heap_extract_index (DzlHeap *heap, + gsize index_, + gpointer result) +{ + DzlHeapReal *real = (DzlHeapReal *)heap; + gssize ipos; + gssize lpos; + gssize mpos; + gssize ppos; + gssize rpos; + + g_return_val_if_fail (heap, FALSE); + g_return_val_if_fail (index_ < G_MAXSSIZE, FALSE); + g_return_val_if_fail (index_ < (gsize)real->len, FALSE); + + if (real->len <= 0) + return FALSE; + + if (result) + memcpy (result, heap_index (real, index_), real->element_size); + + real->len--; + + if (real->len > 0 && index_ != (gsize)real->len) + { + memcpy (heap_index (real, index_), + heap_index (real, real->len), + real->element_size); + + ipos = index_; + ppos = heap_parent (ipos); + + while (heap_compare (real, ipos, ppos) > 0) + { + heap_swap (real, ipos, ppos); + ipos = ppos; + ppos = heap_parent (ppos); + } + + if (ipos == (gssize)index_) + { + while (TRUE) + { + lpos = heap_left (ipos); + rpos = heap_right (ipos); + + if ((lpos < real->len) && (heap_compare (real, lpos, ipos) > 0)) + mpos = lpos; + else + mpos = ipos; + + if ((rpos < real->len) && (heap_compare (real, rpos, mpos) > 0)) + mpos = rpos; + + if (mpos == ipos) + break; + + heap_swap (real, mpos, ipos); + + ipos = mpos; + } + } + } + + if ((real->len > MIN_HEAP_SIZE) && (real->allocated_len / 2) >= (gsize)real->len) + dzl_heap_real_shrink (real); + + return TRUE; +} diff --git a/src/util/dzl-heap.h b/src/util/dzl-heap.h new file mode 100644 index 0000000..fa47550 --- /dev/null +++ b/src/util/dzl-heap.h @@ -0,0 +1,64 @@ +/* dzl-heap.h + * + * Copyright (C) 2014-2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_HEAP_H +#define DZL_HEAP_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_HEAP (dzl_heap_get_type()) +#define dzl_heap_insert_val(h,v) dzl_heap_insert_vals(h,&(v),1) +#define dzl_heap_index(h,t,i) (((t*)(void*)(h)->data)[i]) +#define dzl_heap_peek(h,t) dzl_heap_index(h,t,0) + +typedef struct _DzlHeap DzlHeap; + +struct _DzlHeap +{ + gchar *data; + gsize len; +}; + +DZL_AVAILABLE_IN_ALL +GType dzl_heap_get_type (void); +DZL_AVAILABLE_IN_ALL +DzlHeap *dzl_heap_new (guint element_size, + GCompareFunc compare_func); +DZL_AVAILABLE_IN_ALL +DzlHeap *dzl_heap_ref (DzlHeap *heap); +DZL_AVAILABLE_IN_ALL +void dzl_heap_unref (DzlHeap *heap); +DZL_AVAILABLE_IN_ALL +void dzl_heap_insert_vals (DzlHeap *heap, + gconstpointer data, + guint len); +DZL_AVAILABLE_IN_ALL +gboolean dzl_heap_extract (DzlHeap *heap, + gpointer result); +DZL_AVAILABLE_IN_ALL +gboolean dzl_heap_extract_index (DzlHeap *heap, + gsize index_, + gpointer result); + +G_END_DECLS + +#endif /* DZL_HEAP_H */ diff --git a/src/util/dzl-int-pair.h b/src/util/dzl-int-pair.h new file mode 100644 index 0000000..b66992b --- /dev/null +++ b/src/util/dzl-int-pair.h @@ -0,0 +1,209 @@ +/* dzl-int-pair.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_INT_PAIR_H +#define DZL_INT_PAIR_H + +#ifndef __GI_SCANNER__ + +#include +#include + +G_BEGIN_DECLS + +#if GLIB_SIZEOF_VOID_P == 8 +# define DZL_INT_PAIR_64 +#endif + +#ifdef DZL_INT_PAIR_64 + +typedef union +{ + /*< private >*/ + struct { + gint first; + gint second; + }; + gpointer ptr; +} DzlIntPair; + +typedef union +{ + /*< private >*/ + struct { + guint first; + guint second; + }; + gpointer ptr; +} DzlUIntPair; + +#else + +typedef struct +{ + /*< private >*/ + gint first; + gint second; +} DzlIntPair; + +typedef struct +{ + /*< private >*/ + guint first; + guint second; +} DzlUIntPair; + +#endif + +/** + * dzl_int_pair_new: (skip) + */ +static inline DzlIntPair * +dzl_int_pair_new (gint first, gint second) +{ + DzlIntPair pair; + + /* Avoid tripping g-ir-scanner by putting this + * inside the inline function. + */ + G_STATIC_ASSERT (sizeof (DzlIntPair) == 8); + + pair.first = first; + pair.second = second; + +#ifdef DZL_INT_PAIR_64 + return pair.ptr; +#else + return g_slice_copy (sizeof (DzlIntPair), &pair); +#endif +} + +/** + * dzl_uint_pair_new: (skip) + */ +static inline DzlUIntPair * +dzl_uint_pair_new (guint first, guint second) +{ + DzlUIntPair pair; + + /* Avoid tripping g-ir-scanner by putting this + * inside the inline function. + */ + G_STATIC_ASSERT (sizeof (DzlUIntPair) == 8); + + pair.first = first; + pair.second = second; + +#ifdef DZL_INT_PAIR_64 + return pair.ptr; +#else + return g_slice_copy (sizeof (DzlUIntPair), &pair); +#endif +} + +/** + * dzl_int_pair_first: (skip) + */ +static inline gint +dzl_int_pair_first (DzlIntPair *pair) +{ + DzlIntPair p; +#ifdef DZL_INT_PAIR_64 + p.ptr = pair; +#else + p = *pair; +#endif + return p.first; +} + +/** + * dzl_int_pair_second: (skip) + */ +static inline gint +dzl_int_pair_second (DzlIntPair *pair) +{ + DzlIntPair p; +#ifdef DZL_INT_PAIR_64 + p.ptr = pair; +#else + p = *pair; +#endif + return p.second; +} + +/** + * dzl_uint_pair_first: (skip) + */ +static inline guint +dzl_uint_pair_first (DzlUIntPair *pair) +{ + DzlUIntPair p; +#ifdef DZL_INT_PAIR_64 + p.ptr = pair; +#else + p = *pair; +#endif + return p.first; +} + +/** + * dzl_uint_pair_second: (skip) + */ +static inline guint +dzl_uint_pair_second (DzlUIntPair *pair) +{ + DzlUIntPair p; +#ifdef DZL_INT_PAIR_64 + p.ptr = pair; +#else + p = *pair; +#endif + return p.second; +} + +/** + * dzl_int_pair_free: (skip) + */ +static inline void +dzl_int_pair_free (DzlIntPair *pair) +{ +#ifdef DZL_INT_PAIR_64 + /* Do Nothing */ +#else + g_slice_free (DzlIntPair, pair); +#endif +} + +/** + * dzl_uint_pair_free: (skip) + */ +static inline void +dzl_uint_pair_free (DzlUIntPair *pair) +{ +#ifdef DZL_INT_PAIR_64 + /* Do Nothing */ +#else + g_slice_free (DzlUIntPair, pair); +#endif +} + +G_END_DECLS + +#endif /* __GI_SCANNER__ */ + +#endif /* DZL_INT_PAIR_H */ diff --git a/src/util/dzl-macros.h b/src/util/dzl-macros.h new file mode 100644 index 0000000..5151e39 --- /dev/null +++ b/src/util/dzl-macros.h @@ -0,0 +1,114 @@ +/* dzl-macros.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_MACROS_H +#define DZL_MACROS_H + +#include +#include + +#ifdef __linux__ +# include +# include +# include +#endif + +G_BEGIN_DECLS + +#if defined(_MSC_VER) +# define DZL_ALIGNED_BEGIN(_N) __declspec(align(_N)) +# define DZL_ALIGNED_END(_N) +#else +# define DZL_ALIGNED_BEGIN(_N) +# define DZL_ALIGNED_END(_N) __attribute__((aligned(_N))) +#endif + +#define dzl_clear_weak_pointer(ptr) \ + (*(ptr) ? (g_object_remove_weak_pointer((GObject*)*(ptr), (gpointer*)ptr),*(ptr)=NULL,1) : 0) + +#define dzl_set_weak_pointer(ptr,obj) \ + ((obj!=*(ptr))?(dzl_clear_weak_pointer(ptr),*(ptr)=obj,((obj)?g_object_add_weak_pointer((GObject*)obj,(gpointer*)ptr),NULL:NULL),1):0) + +/* strlen() gets hoisted out automatically at -O0 for everything but MSVC */ +#define DZL_LITERAL_LENGTH(s) (strlen(s)) + +static inline void +dzl_clear_signal_handler (gpointer object, + gulong *location_of_handler) +{ + if (*location_of_handler != 0) + { + gulong handler = *location_of_handler; + *location_of_handler = 0; + g_signal_handler_disconnect (object, handler); + } +} + +static inline gboolean +dzl_str_empty0 (gconstpointer str) +{ + /* We use a gconstpointer to allow passing both + * signed and unsigned chars into this function */ + return str == NULL || *(char*)str == '\0'; +} + +static inline gboolean +dzl_str_equal0 (gconstpointer str1, + gconstpointer str2) +{ + /* We use gconstpointer so that we can allow + * both signed and unsigned chars here (such as xmlChar). */ + return g_strcmp0 (str1, str2) == 0; +} + +static inline void +dzl_clear_source (guint *source_ptr) +{ + guint source = *source_ptr; + *source_ptr = 0; + if (source != 0) + g_source_remove (source); +} + +static inline void +dzl_assert_is_main_thread (void) +{ +#ifndef G_DISABLE_ASSERT +# ifdef __linux__ + static GThread *main_thread; + GThread *self = g_thread_self (); + + if G_LIKELY (main_thread == self) + return; + + /* Slow path, rely on task id == process id */ + if ((pid_t)syscall (SYS_gettid) == getpid ()) + { + /* Allow for fast path next time */ + main_thread = self; + return; + } + + g_assert_not_reached (); +# endif +#endif +} + +G_END_DECLS + +#endif /* DZL_MACROS_H */ diff --git a/src/util/dzl-pango.c b/src/util/dzl-pango.c new file mode 100644 index 0000000..be88a9d --- /dev/null +++ b/src/util/dzl-pango.c @@ -0,0 +1,202 @@ +/* dzl-pango.c + * + * Copyright (C) 2014-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-pango" + +#include "config.h" + +#include +#include +#include + +#include "util/dzl-pango.h" + +#define FONT_FAMILY "font-family" +#define FONT_VARIANT "font-variant" +#define FONT_STRETCH "font-stretch" +#define FONT_WEIGHT "font-weight" +#define FONT_SIZE "font-size" + +/** + * dzl_pango_font_description_to_css: + * + * This function will generate CSS suitable for Gtk's CSS engine + * based on the properties of the #PangoFontDescription. + * + * Returns: (transfer full): A newly allocated string containing the + * CSS describing the font description. + */ +gchar * +dzl_pango_font_description_to_css (const PangoFontDescription *font_desc) +{ + PangoFontMask mask; + GString *str; + +#define ADD_KEYVAL(key,fmt) \ + g_string_append(str,key":"fmt";") +#define ADD_KEYVAL_PRINTF(key,fmt,...) \ + g_string_append_printf(str,key":"fmt";", __VA_ARGS__) + + g_return_val_if_fail (font_desc, NULL); + + str = g_string_new (NULL); + + mask = pango_font_description_get_set_fields (font_desc); + + if ((mask & PANGO_FONT_MASK_FAMILY) != 0) + { + const gchar *family; + + family = pango_font_description_get_family (font_desc); + ADD_KEYVAL_PRINTF (FONT_FAMILY, "\"%s\"", family); + } + + if ((mask & PANGO_FONT_MASK_STYLE) != 0) + { + PangoVariant variant; + + variant = pango_font_description_get_variant (font_desc); + + switch (variant) + { + case PANGO_VARIANT_NORMAL: + ADD_KEYVAL (FONT_VARIANT, "normal"); + break; + + case PANGO_VARIANT_SMALL_CAPS: + ADD_KEYVAL (FONT_VARIANT, "small-caps"); + break; + + default: + break; + } + } + + if ((mask & PANGO_FONT_MASK_WEIGHT)) + { + gint weight; + + weight = pango_font_description_get_weight (font_desc); + + /* + * WORKAROUND: + * + * font-weight with numbers does not appear to be working as expected + * right now. So for the common (bold/normal), let's just use the string + * and let gtk warn for the other values, which shouldn't really be + * used for this. + */ + + switch (weight) + { + case PANGO_WEIGHT_SEMILIGHT: + /* + * 350 is not actually a valid css font-weight, so we will just round + * up to 400. + */ + case PANGO_WEIGHT_NORMAL: + ADD_KEYVAL (FONT_WEIGHT, "normal"); + break; + + case PANGO_WEIGHT_BOLD: + ADD_KEYVAL (FONT_WEIGHT, "bold"); + break; + + case PANGO_WEIGHT_THIN: + case PANGO_WEIGHT_ULTRALIGHT: + case PANGO_WEIGHT_LIGHT: + case PANGO_WEIGHT_BOOK: + case PANGO_WEIGHT_MEDIUM: + case PANGO_WEIGHT_SEMIBOLD: + case PANGO_WEIGHT_ULTRABOLD: + case PANGO_WEIGHT_HEAVY: + case PANGO_WEIGHT_ULTRAHEAVY: + default: + /* round to nearest hundred */ + weight = round (weight / 100.0) * 100; + ADD_KEYVAL_PRINTF ("font-weight", "%d", weight); + break; + } + } + +#ifndef GDK_WINDOWING_QUARTZ + /* + * We seem to get "Condensed" for fonts on the Quartz backend, + * which is rather annoying as it results in us always hitting + * fallback (stretch) paths. So let's cheat and just disable + * stretch support for now on Quartz. + */ + if ((mask & PANGO_FONT_MASK_STRETCH)) + { + switch (pango_font_description_get_stretch (font_desc)) + { + case PANGO_STRETCH_ULTRA_CONDENSED: + ADD_KEYVAL (FONT_STRETCH, "untra-condensed"); + break; + + case PANGO_STRETCH_EXTRA_CONDENSED: + ADD_KEYVAL (FONT_STRETCH, "extra-condensed"); + break; + + case PANGO_STRETCH_CONDENSED: + ADD_KEYVAL (FONT_STRETCH, "condensed"); + break; + + case PANGO_STRETCH_SEMI_CONDENSED: + ADD_KEYVAL (FONT_STRETCH, "semi-condensed"); + break; + + case PANGO_STRETCH_NORMAL: + ADD_KEYVAL (FONT_STRETCH, "normal"); + break; + + case PANGO_STRETCH_SEMI_EXPANDED: + ADD_KEYVAL (FONT_STRETCH, "semi-expanded"); + break; + + case PANGO_STRETCH_EXPANDED: + ADD_KEYVAL (FONT_STRETCH, "expanded"); + break; + + case PANGO_STRETCH_EXTRA_EXPANDED: + ADD_KEYVAL (FONT_STRETCH, "extra-expanded"); + break; + + case PANGO_STRETCH_ULTRA_EXPANDED: + ADD_KEYVAL (FONT_STRETCH, "untra-expanded"); + break; + + default: + break; + } + } +#endif + + if ((mask & PANGO_FONT_MASK_SIZE)) + { + gint font_size; + + font_size = pango_font_description_get_size (font_desc) / PANGO_SCALE; + ADD_KEYVAL_PRINTF ("font-size", "%dpt", font_size); + } + + return g_string_free (str, FALSE); + +#undef ADD_KEYVAL +#undef ADD_KEYVAL_PRINTF +} diff --git a/src/util/dzl-pango.h b/src/util/dzl-pango.h new file mode 100644 index 0000000..8f28029 --- /dev/null +++ b/src/util/dzl-pango.h @@ -0,0 +1,33 @@ +/* dzl-pango.h + * + * Copyright (C) 2014 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PANGO_H +#define DZL_PANGO_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +gchar *dzl_pango_font_description_to_css (const PangoFontDescription *font_desc); + +G_END_DECLS + +#endif /* DZL_PANGO_H */ diff --git a/src/util/dzl-rgba.c b/src/util/dzl-rgba.c new file mode 100644 index 0000000..56e1287 --- /dev/null +++ b/src/util/dzl-rgba.c @@ -0,0 +1,221 @@ +/* dzl-rgba.c + * + * Copyright (C) 2014 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-rgba" + +#include "config.h" + +#include "dzl-rgba.h" + +/* + * The following code is borrowed ultimately upstream through Clearlooks + * theme engine in 2.x days. It has been ported to use GdkRGBA. It exists + * all over the place, feel free to use this under any license compatible + * with upstream. + */ + +/* Clearlooks - a cairo based GTK+ engine + * Copyright (C) 2006 Andrew Johnson + * Copyright (C) 2007 Benjamin Berg + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +static void +rgb_to_hls (gdouble *r, + gdouble *g, + gdouble *b) +{ + gdouble min; + gdouble max; + gdouble red; + gdouble green; + gdouble blue; + gdouble h, l, s; + gdouble delta; + + red = *r; + green = *g; + blue = *b; + if (red > green) + { + if (red > blue) + max = red; + else + max = blue; + if (green < blue) + min = green; + else + min = blue; + } + else + { + if (green > blue) + max = green; + else + max = blue; + if (red < blue) + min = red; + else + min = blue; + } + l = (max + min) / 2; + s = 0; + h = 0; + if (max != min) + { + if (l <= 0.5) + s = (max - min) / (max + min); + else + s = (max - min) / (2 - max - min); + delta = max - min; + if (red == max) + h = (green - blue) / delta; + else if (green == max) + h = 2 + (blue - red) / delta; + else if (blue == max) + h = 4 + (red - green) / delta; + h *= 60; + if (h < 0.0) + h += 360; + } + *r = h; + *g = l; + *b = s; +} + +static void +hls_to_rgb (gdouble *h, + gdouble *l, + gdouble *s) +{ + gdouble hue; + gdouble lightness; + gdouble saturation; + gdouble m1, m2; + gdouble r, g, b; + + lightness = *l; + saturation = *s; + if (lightness <= 0.5) + m2 = lightness * (1 + saturation); + else + m2 = lightness + saturation - lightness * saturation; + m1 = 2 * lightness - m2; + if (saturation == 0) + { + *h = lightness; + *l = lightness; + *s = lightness; + } + else + { + hue = *h + 120; + while (hue > 360) + hue -= 360; + while (hue < 0) + hue += 360; + if (hue < 60) + r = m1 + (m2 - m1) * hue / 60; + else if (hue < 180) + r = m2; + else if (hue < 240) + r = m1 + (m2 - m1) * (240 - hue) / 60; + else + r = m1; + hue = *h; + while (hue > 360) + hue -= 360; + while (hue < 0) + hue += 360; + if (hue < 60) + g = m1 + (m2 - m1) * hue / 60; + else if (hue < 180) + g = m2; + else if (hue < 240) + g = m1 + (m2 - m1) * (240 - hue) / 60; + else + g = m1; + hue = *h - 120; + while (hue > 360) + hue -= 360; + while (hue < 0) + hue += 360; + if (hue < 60) + b = m1 + (m2 - m1) * hue / 60; + else if (hue < 180) + b = m2; + else if (hue < 240) + b = m1 + (m2 - m1) * (240 - hue) / 60; + else + b = m1; + *h = r; + *l = g; + *s = b; + } +} + +void +dzl_rgba_shade (const GdkRGBA *rgba, + GdkRGBA *dst, + gdouble k) +{ + gdouble red; + gdouble green; + gdouble blue; + + red = rgba->red; + green = rgba->green; + blue = rgba->blue; + + rgb_to_hls (&red, &green, &blue); + + green *= k; + + if (green > 1.0) + green = 1.0; + else if (green < 0.0) + green = 0.0; + + blue *= k; + + if (blue > 1.0) + blue = 1.0; + else if (blue < 0.0) + blue = 0.0; + + hls_to_rgb (&red, &green, &blue); + + dst->red = red; + dst->green = green; + dst->blue = blue; + dst->alpha = rgba->alpha; +} diff --git a/src/util/dzl-rgba.h b/src/util/dzl-rgba.h new file mode 100644 index 0000000..0bd3d1b --- /dev/null +++ b/src/util/dzl-rgba.h @@ -0,0 +1,35 @@ +/* dzl-rgba.h + * + * Copyright (C) 2014 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_RGBA_H +#define DZL_RGBA_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +void dzl_rgba_shade (const GdkRGBA *rgba, + GdkRGBA *dst, + gdouble k); + +G_END_DECLS + +#endif /* DZL_RGBA_H */ diff --git a/src/util/dzl-ring.c b/src/util/dzl-ring.c new file mode 100644 index 0000000..9a731cd --- /dev/null +++ b/src/util/dzl-ring.c @@ -0,0 +1,211 @@ +/* dzl-ring.c + * + * Copyright (C) 2010 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-ring" + +#include "config.h" + +#include + +#include "dzl-ring.h" + +#define get_element(r,i) ((r)->data + ((r)->elt_size * i)) + +typedef struct +{ + /*< public >*/ + guint8 *data; /* Pointer to real data. */ + guint len; /* Length of data allocation. */ + guint pos; /* Position in ring. */ + + /*< private >*/ + guint elt_size; /* Size of each element. */ + gboolean looped; /* Have we wrapped around at least once. */ + GDestroyNotify destroy; /* Destroy element callback. */ + volatile gint ref_count; /* Hidden Reference count. */ +} DzlRingImpl; + +G_DEFINE_BOXED_TYPE (DzlRing, dzl_ring, dzl_ring_ref, dzl_ring_unref) + +/** + * dzl_ring_sized_new: + * @element_size: (in): The size per element. + * @reserved_size: (in): The number of elements to allocate. + * @element_destroy: (in): Notification called when removing an element. + * + * Creates a new instance of #DzlRing with the given number of elements. + * + * Returns: A new #DzlRing. + */ +DzlRing* +dzl_ring_sized_new (guint element_size, + guint reserved_size, + GDestroyNotify element_destroy) +{ + DzlRingImpl *ring_impl; + + ring_impl = g_slice_new0 (DzlRingImpl); + ring_impl->elt_size = element_size; + ring_impl->len = reserved_size; + ring_impl->data = g_malloc0_n (reserved_size, element_size); + ring_impl->destroy = element_destroy; + ring_impl->ref_count = 1; + + return (DzlRing *)ring_impl; +} + +/** + * dzl_ring_append_vals: + * @ring: (in): A #DzlRing. + * @data: (in): A pointer to the array of values. + * @len: (in): The number of values. + * + * Appends @len values located at @data. + * + * Returns: the index of the first item. + */ +guint +dzl_ring_append_vals (DzlRing *ring, + gconstpointer data, + guint len) +{ + DzlRingImpl *ring_impl = (DzlRingImpl *)ring; + gpointer idx; + gint ret = -1; + gint x; + + g_return_val_if_fail (ring_impl != NULL, 0); + g_return_val_if_fail (len <= ring->len, 0); + g_return_val_if_fail (len > 0, 0); + g_return_val_if_fail (len <= G_MAXINT, 0); + + for (gint i = 0; i < (gint)len; i++) + { + x = ring->pos - i; + x = (x >= 0) ? x : (gint)ring->len + x; + idx = ring->data + (ring_impl->elt_size * x); + if (ring_impl->destroy && (ring_impl->looped == TRUE)) + ring_impl->destroy (idx); + if (ret == -1) + ret = x; + memcpy (idx, data, ring_impl->elt_size); + ring->pos++; + if (ring->pos >= ring->len) + ring_impl->looped = TRUE; + ring->pos %= ring->len; + data = ((guint8 *)data) + ring_impl->elt_size; + } + + return (guint)ret; +} + +/** + * dzl_ring_foreach: + * @ring: (in): A #DzlRing. + * @func: (in) (scope call): A #GFunc to call for each element. + * @user_data: (in): user data for @func. + * + * Calls @func for every item in the #DzlRing starting from the most recently + * inserted element to the least recently inserted. + */ +void +dzl_ring_foreach (DzlRing *ring, + GFunc func, + gpointer user_data) +{ + DzlRingImpl *ring_impl = (DzlRingImpl *)ring; + gint i; + + g_return_if_fail (ring_impl != NULL); + g_return_if_fail (func != NULL); + + if (!ring_impl->looped) + { + for (i = 0; i < (gint)ring_impl->pos; i++) + func (get_element (ring_impl, i), user_data); + return; + } + + for (i = ring_impl->pos; i < (gint)ring_impl->len; i++) + func (get_element (ring_impl, i), user_data); + + for (i = 0; i < (gint)ring_impl->pos; i++) + func (get_element (ring_impl, i), user_data); +} + +/** + * dzl_ring_destroy: + * @ring: (in): A #DzlRing. + * + * Cleans up after a #DzlRing that is no longer in use. + */ +void +dzl_ring_destroy (DzlRing *ring) +{ + DzlRingImpl *ring_impl = (DzlRingImpl *)ring; + + g_return_if_fail (ring != NULL); + g_return_if_fail (ring_impl->ref_count == 0); + + if (ring_impl->destroy != NULL) + dzl_ring_foreach (ring, (GFunc)ring_impl->destroy, NULL); + + g_free (ring_impl->data); + + g_slice_free (DzlRingImpl, ring_impl); +} + +/** + * dzl_ring_ref: + * @ring: (in): A #DzlRing. + * + * Atomically increments the reference count of @ring by one. + * + * Returns: The @ring pointer. + */ +DzlRing * +dzl_ring_ref (DzlRing *ring) +{ + DzlRingImpl *ring_impl = (DzlRingImpl *)ring; + + g_return_val_if_fail (ring != NULL, NULL); + g_return_val_if_fail (ring_impl->ref_count > 0, NULL); + + g_atomic_int_inc (&ring_impl->ref_count); + + return ring; +} + +/** + * dzl_ring_unref: + * @ring: (in): A #DzlRing. + * + * Atomically decrements the reference count of @ring by one. When the + * reference count reaches zero, the structure is freed. + */ +void +dzl_ring_unref (DzlRing *ring) +{ + DzlRingImpl *ring_impl = (DzlRingImpl *)ring; + + g_return_if_fail (ring != NULL); + g_return_if_fail (ring_impl->ref_count > 0); + + if (g_atomic_int_dec_and_test (&ring_impl->ref_count)) + dzl_ring_destroy (ring); +} diff --git a/src/util/dzl-ring.h b/src/util/dzl-ring.h new file mode 100644 index 0000000..b6e31ea --- /dev/null +++ b/src/util/dzl-ring.h @@ -0,0 +1,108 @@ +/* dzl-ring.h + * + * Copyright (C) 2010 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef __DZL_RING_H__ +#define __DZL_RING_H__ + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +/** + * dzl_ring_append_val: + * @ring: A #DzlRing. + * @val: A value to append to the #DzlRing. + * + * Appends a value to the ring buffer. @val must be a variable as it is + * referenced to. + * + * Returns: None. + */ +#define dzl_ring_append_val(ring, val) dzl_ring_append_vals(ring, &(val), 1) + +/** + * _dzl_ring_index: (skip) + * + * Used to convert an index to valid indx within the ring. We only support + * offsets within +/- one range of the length of the ring. + */ +#define _dzl_ring_index(ring, i) \ + ({ \ + gint __idx = (gint)(ring->pos) + i; \ + if (__idx < 0) \ + __idx += (gint)ring->len; \ + else if (__idx >= (gint)(ring)->len) \ + __idx -= (gint)ring->len; \ + __idx; \ + }) + +/** + * dzl_ring_get_index: + * @ring: A #DzlRing. + * @type: The type to extract. + * @i: The index within the #DzlRing relative to the current position. + * + * Retrieves the value at the given index from the #DzlRing. The value + * is cast to @type. You may retrieve a pointer to the value within the + * array by using &. + * + * [[ + * gdouble *v = &dzl_ring_get_index(ring, gdouble, 0); + * gdouble v = dzl_ring_get_index(ring, gdouble, 0); + * ]] + * + * Returns: The value at the given index. + */ +#define dzl_ring_get_index(ring, type, i) \ + ((((type *)(gpointer)(ring)->data))[_dzl_ring_index(ring, i)]) + +typedef struct +{ + guint8 *data; + guint len; + guint pos; + + /*< private >*/ +} DzlRing; + +DZL_AVAILABLE_IN_ALL +GType dzl_ring_get_type (void); +DZL_AVAILABLE_IN_ALL +DzlRing *dzl_ring_sized_new (guint element_size, + guint reserved_size, + GDestroyNotify element_destroy); +DZL_AVAILABLE_IN_ALL +guint dzl_ring_append_vals (DzlRing *ring, + gconstpointer data, + guint len); +DZL_AVAILABLE_IN_ALL +void dzl_ring_foreach (DzlRing *ring, + GFunc func, + gpointer user_data); +DZL_AVAILABLE_IN_ALL +DzlRing *dzl_ring_ref (DzlRing *ring); +DZL_AVAILABLE_IN_ALL +void dzl_ring_unref (DzlRing *ring); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (DzlRing, dzl_ring_unref) + +G_END_DECLS + +#endif /* __DZL_RING_H__ */ diff --git a/src/util/dzl-util-private.h b/src/util/dzl-util-private.h new file mode 100644 index 0000000..b426818 --- /dev/null +++ b/src/util/dzl-util-private.h @@ -0,0 +1,53 @@ +/* dzl-util-private.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_UTIL_PRIVATE_H +#define DZL_UTIL_PRIVATE_H + +#include + +#include "dzl-macros.h" + +G_BEGIN_DECLS + +void dzl_gtk_widget_class_add_css_resource (GtkWidgetClass *widget_class, + const gchar *resource); +void dzl_gtk_widget_add_class (GtkWidget *widget, + const gchar *class_name); +gboolean dzl_gtk_widget_activate_action (GtkWidget *widget, + const gchar *full_action_name, + GVariant *variant); +GVariant *dzl_gtk_widget_get_action_state (GtkWidget *widget, + const gchar *action_name); +GActionGroup *dzl_gtk_widget_find_group_for_action (GtkWidget *widget, + const gchar *action_name); +void dzl_g_action_name_parse (const gchar *action_name, + gchar **prefix, + gchar **name); +gboolean dzl_g_action_name_parse_full (const gchar *detailed_action_name, + gchar **prefix, + gchar **name, + GVariant **target); +void dzl_gtk_style_context_get_borders (GtkStyleContext *style_context, + GtkBorder *borders); +void dzl_gtk_allocation_subtract_border (GtkAllocation *alloc, + GtkBorder *border); + +G_END_DECLS + +#endif /* DZL_UTIL_PRIVATE_H */ diff --git a/src/util/dzl-util.c b/src/util/dzl-util.c new file mode 100644 index 0000000..c19d949 --- /dev/null +++ b/src/util/dzl-util.c @@ -0,0 +1,362 @@ +/* dzl-util.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-util" + +#include "config.h" + +#include + +#include "dzl-util-private.h" + +static void +dzl_gtk_border_sum (GtkBorder *one, + const GtkBorder *two) +{ + one->top += two->top; + one->right += two->right; + one->bottom += two->bottom; + one->left += two->left; +} + +void +dzl_gtk_style_context_get_borders (GtkStyleContext *style_context, + GtkBorder *borders) +{ + GtkBorder border = { 0 }; + GtkBorder padding = { 0 }; + GtkBorder margin = { 0 }; + GtkStateFlags state; + + g_return_if_fail (GTK_IS_STYLE_CONTEXT (style_context)); + g_return_if_fail (borders != NULL); + + memset (borders, 0, sizeof *borders); + + state = gtk_style_context_get_state (style_context); + + gtk_style_context_get_border (style_context, state, &border); + gtk_style_context_get_padding (style_context, state, &padding); + gtk_style_context_get_margin (style_context, state, &margin); + + dzl_gtk_border_sum (borders, &padding); + dzl_gtk_border_sum (borders, &border); + dzl_gtk_border_sum (borders, &margin); +} + +static void +split_action_name (const gchar *action_name, + gchar **prefix, + gchar **name) +{ + const gchar *dot; + + g_assert (prefix != NULL); + g_assert (name != NULL); + + *prefix = NULL; + *name = NULL; + + if (action_name == NULL) + return; + + dot = strchr (action_name, '.'); + + if (dot == NULL) + *name = g_strdup (action_name); + else + { + *prefix = g_strndup (action_name, dot - action_name); + *name = g_strdup (dot + 1); + } +} + +gboolean +dzl_gtk_widget_activate_action (GtkWidget *widget, + const gchar *full_action_name, + GVariant *parameter) +{ + GtkWidget *toplevel; + GApplication *app; + GActionGroup *group = NULL; + gchar *prefix = NULL; + gchar *action_name = NULL; + const gchar *dot; + gboolean ret = FALSE; + + g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE); + g_return_val_if_fail (full_action_name, FALSE); + + dot = strchr (full_action_name, '.'); + + if (dot == NULL) + { + prefix = NULL; + action_name = g_strdup (full_action_name); + } + else + { + prefix = g_strndup (full_action_name, dot - full_action_name); + action_name = g_strdup (dot + 1); + } + + /* + * TODO: Support non-grouped names. We need to walk + * through all the groups at each level to do this. + */ + if (prefix == NULL) + prefix = g_strdup ("win"); + + app = g_application_get_default (); + toplevel = gtk_widget_get_toplevel (widget); + + while ((group == NULL) && (widget != NULL)) + { + group = gtk_widget_get_action_group (widget, prefix); + + if G_UNLIKELY (GTK_IS_POPOVER (widget)) + { + GtkWidget *relative_to; + + relative_to = gtk_popover_get_relative_to (GTK_POPOVER (widget)); + + if (relative_to != NULL) + widget = relative_to; + else + widget = gtk_widget_get_parent (widget); + } + else + { + widget = gtk_widget_get_parent (widget); + } + } + + if (!group && g_str_equal (prefix, "win") && G_IS_ACTION_GROUP (toplevel)) + group = G_ACTION_GROUP (toplevel); + + if (!group && g_str_equal (prefix, "app") && G_IS_ACTION_GROUP (app)) + group = G_ACTION_GROUP (app); + + if (group && g_action_group_has_action (group, action_name)) + { + g_action_group_activate_action (group, action_name, parameter); + ret = TRUE; + goto cleanup; + } + + if (parameter && g_variant_is_floating (parameter)) + { + parameter = g_variant_ref_sink (parameter); + g_variant_unref (parameter); + } + + g_warning ("Failed to locate action %s.%s", prefix, action_name); + +cleanup: + g_free (prefix); + g_free (action_name); + + return ret; +} + +static GActionGroup * +find_group_with_action (GtkWidget *widget, + const gchar *prefix, + const gchar *name) +{ + GActionGroup *group; + + g_assert (GTK_IS_WIDGET (widget)); + g_assert (name != NULL); + + /* + * GtkWidget does not provide a way to get group names, + * so there is nothing more we can do if prefix is NULL. + */ + if (prefix == NULL) + return NULL; + + if (g_str_equal (prefix, "app")) + group = G_ACTION_GROUP (g_application_get_default ()); + else + group = gtk_widget_get_action_group (widget, prefix); + + if (group != NULL && g_action_group_has_action (group, name)) + return group; + + widget = gtk_widget_get_parent (widget); + + if (widget != NULL) + return find_group_with_action (widget, prefix, name); + + return NULL; +} + +GVariant * +dzl_gtk_widget_get_action_state (GtkWidget *widget, + const gchar *action_name) +{ + GActionGroup *group; + gchar *prefix = NULL; + gchar *name = NULL; + GVariant *ret = NULL; + + split_action_name (action_name, &prefix, &name); + if (name == NULL || prefix == NULL) + goto cleanup; + + group = find_group_with_action (widget, prefix, name); + if (group == NULL) + goto cleanup; + + ret = g_action_group_get_action_state (group, name); + +cleanup: + g_free (name); + g_free (prefix); + + return ret; +} + +GActionGroup * +dzl_gtk_widget_find_group_for_action (GtkWidget *widget, + const gchar *action_name) +{ + GActionGroup *group = NULL; + gchar *prefix = NULL; + gchar *name = NULL; + + g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL); + + if (action_name == NULL) + return NULL; + + split_action_name (action_name, &prefix, &name); + if (name == NULL || prefix == NULL) + goto cleanup; + + group = find_group_with_action (widget, prefix, name); + +cleanup: + g_free (name); + g_free (prefix); + + return group; +} + +void +dzl_g_action_name_parse (const gchar *action_name, + gchar **prefix, + gchar **name) +{ + split_action_name (action_name, prefix, name); +} + +gboolean +dzl_g_action_name_parse_full (const gchar *detailed_action_name, + gchar **prefix, + gchar **name, + GVariant **target) +{ + g_autofree gchar *full_name = NULL; + g_autoptr(GVariant) target_value = NULL; + const gchar *dot; + + if (detailed_action_name == NULL) + return FALSE; + + if (!g_action_parse_detailed_name (detailed_action_name, &full_name, &target_value, NULL)) + return FALSE; + + if (target_value != NULL) + g_variant_take_ref (target_value); + + dot = strchr (full_name, '.'); + + if (dot != NULL) + { + if (prefix != NULL) + *prefix = g_strndup (full_name, dot - full_name); + + if (name != NULL) + *name = g_strdup (dot + 1); + } + else + { + *prefix = NULL; + *name = g_steal_pointer (&full_name); + } + + if (target != NULL) + *target = g_steal_pointer (&target_value); + + return TRUE; +} + +void +dzl_gtk_allocation_subtract_border (GtkAllocation *alloc, + GtkBorder *border) +{ + g_return_if_fail (alloc != NULL); + g_return_if_fail (border != NULL); + + alloc->x += border->left; + alloc->y += border->top; + alloc->width -= (border->left + border->right); + alloc->height -= (border->top + border->bottom); +} + +void +dzl_gtk_widget_add_class (GtkWidget *widget, + const gchar *class_name) +{ + gtk_style_context_add_class (gtk_widget_get_style_context (widget), class_name); +} + +void +dzl_gtk_widget_class_add_css_resource (GtkWidgetClass *widget_class, + const gchar *resource) +{ + GdkScreen *screen = gdk_screen_get_default (); + + g_return_if_fail (widget_class != NULL); + g_return_if_fail (resource != NULL); + + if (screen != NULL) + { + g_autoptr(GtkCssProvider) provider = NULL; + + /* + * It would be nice if our theme data could be more theme friendly. + * However, we need to be higher than SETTINGS so that some of our + * stuff takes effect, but that is already higher than theming. + * + * So really the only proper answer is to get themes to style us and + * eventually drop all CSS. Given that is impossible... I'm not sure + * what other options we really have. Some themes will just need to + * !important or whatever when it matters. + */ + + provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (provider, resource); + gtk_style_context_add_provider_for_screen (screen, + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_SETTINGS + 50); + } + +} diff --git a/src/util/dzl-variant.c b/src/util/dzl-variant.c new file mode 100644 index 0000000..dc2cb72 --- /dev/null +++ b/src/util/dzl-variant.c @@ -0,0 +1,45 @@ +/* dzl-variant.c + * + * Copyright (C) 2016-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-variant" + +#include "config.h" + +#include "util/dzl-variant.h" + +guint +dzl_g_variant_hash (gconstpointer data) +{ + GVariant *variant = (GVariant *)data; + GBytes *bytes; + guint ret; + + if (!g_variant_is_container (variant)) + return g_variant_hash (variant); + + /* Generally we wouldn't want to create a bytes to hash + * during a hash call, since that'd be fairly expensive. + * But since GHashTable caches hash values, its not that + * big of a deal. + */ + bytes = g_variant_get_data_as_bytes (variant); + ret = g_bytes_hash (bytes); + g_bytes_unref (bytes); + + return ret; +} diff --git a/src/util/dzl-variant.h b/src/util/dzl-variant.h new file mode 100644 index 0000000..31f61b2 --- /dev/null +++ b/src/util/dzl-variant.h @@ -0,0 +1,33 @@ +/* dzl-variant.h + * + * Copyright (C) 2016-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_VARIANT_H +#define DZL_VARIANT_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +DZL_AVAILABLE_IN_ALL +guint dzl_g_variant_hash (gconstpointer data); + +G_END_DECLS + +#endif /* DZL_VARIANT_H */ diff --git a/src/util/meson.build b/src/util/meson.build new file mode 100644 index 0000000..f953b5f --- /dev/null +++ b/src/util/meson.build @@ -0,0 +1,45 @@ +util_headers = [ + 'dzl-cairo.h', + 'dzl-cancellable.h', + 'dzl-date-time.h', + 'dzl-dnd.h', + 'dzl-file-manager.h', + 'dzl-gdk.h', + 'dzl-gtk.h', + 'dzl-heap.h', + 'dzl-int-pair.h', + 'dzl-macros.h', + 'dzl-pango.h', + 'dzl-rgba.h', + 'dzl-ring.h', + 'dzl-variant.h', +] + +util_sources = [ + 'dzl-cairo.c', + 'dzl-cancellable.c', + 'dzl-date-time.c', + 'dzl-dnd.c', + 'dzl-file-manager.c', + 'dzl-gdk.c', + 'dzl-gtk.c', + 'dzl-heap.c', + 'dzl-pango.c', + 'dzl-rgba.c', + 'dzl-ring.c', + 'dzl-util.c', + 'dzl-variant.c', +] + +# No counters if building on win32 for now. +# They need explicit porting to that platform and should +# probably just wrap the eventtrace API or something. +if host_machine.system() != 'windows' + util_headers += ['dzl-counter.h'] + util_sources += ['dzl-counter.c'] +endif + +libdazzle_public_headers += files(util_headers) +libdazzle_public_sources += files(util_sources) + +install_headers(util_headers, subdir: join_paths(libdazzle_header_subdir, 'util')) diff --git a/src/widgets/dzl-bin.c b/src/widgets/dzl-bin.c new file mode 100644 index 0000000..ed46e1f --- /dev/null +++ b/src/widgets/dzl-bin.c @@ -0,0 +1,182 @@ +/* dzl-bin.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-bin" + +#include "config.h" + +/** + * SECTION:dzl-bin + * @title: DzlBin + * + * This is just a #GtkBin class that also allows for various styling with + * CSS over what can be done in GtkBin directly. + */ + +#include + +#include "widgets/dzl-bin.h" +#include "util/dzl-util-private.h" + +G_DEFINE_TYPE (DzlBin, dzl_bin, GTK_TYPE_BIN) + +static gboolean +dzl_bin_draw (GtkWidget *widget, + cairo_t *cr) +{ + GtkStyleContext *style_context; + GtkWidget *child; + GtkAllocation alloc; + GtkStateFlags state; + GtkBorder margin; + + g_assert (DZL_IS_BIN (widget)); + + gtk_widget_get_allocation (widget, &alloc); + alloc.x = 0; + alloc.y = 0; + + style_context = gtk_widget_get_style_context (widget); + state = gtk_widget_get_state_flags (widget); + + gtk_style_context_get_margin (style_context, state, &margin); + dzl_gtk_allocation_subtract_border (&alloc, &margin); + + gtk_render_background (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height); + + child = gtk_bin_get_child (GTK_BIN (widget)); + if (child != NULL) + gtk_container_propagate_draw (GTK_CONTAINER (widget), child, cr); + + gtk_render_frame (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height); + + return FALSE; +} + +static void +dzl_bin_size_allocate (GtkWidget *widget, + GtkAllocation *alloc) +{ + DzlBin *self = (DzlBin *)widget; + GtkAllocation child_alloc = { 0 }; + GtkWidget *child; + + g_assert (DZL_IS_BIN (self)); + g_assert (alloc != NULL); + + child = gtk_bin_get_child (GTK_BIN (self)); + + if (child != NULL) + { + GtkStyleContext *style_context; + GtkBorder borders; + + style_context = gtk_widget_get_style_context (widget); + child_alloc = *alloc; + + if (gtk_widget_get_has_window (widget)) + { + child_alloc.x = 0; + child_alloc.y = 0; + } + + dzl_gtk_style_context_get_borders (style_context, &borders); + dzl_gtk_allocation_subtract_border (&child_alloc, &borders); + } + + GTK_WIDGET_CLASS (dzl_bin_parent_class)->size_allocate (widget, alloc); + + if (child != NULL) + gtk_widget_size_allocate (child, &child_alloc); +} + +static void +dzl_bin_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + DzlBin *self = (DzlBin *)widget; + GtkStyleContext *style_context; + GtkWidget *child; + GtkBorder borders; + + g_assert (DZL_IS_BIN (widget)); + + *min_width = 0; + *nat_width = 0; + + child = gtk_bin_get_child (GTK_BIN (self)); + if (child != NULL) + gtk_widget_get_preferred_width (child, min_width, nat_width); + + style_context = gtk_widget_get_style_context (widget); + dzl_gtk_style_context_get_borders (style_context, &borders); + + *min_width += (borders.left + borders.right); + *nat_width += (borders.left + borders.right); +} + +static void +dzl_bin_get_preferred_height (GtkWidget *widget, + gint *min_height, + gint *nat_height) +{ + DzlBin *self = (DzlBin *)widget; + GtkStyleContext *style_context; + GtkWidget *child; + GtkBorder borders; + + g_assert (DZL_IS_BIN (widget)); + + *min_height = 0; + *nat_height = 0; + + child = gtk_bin_get_child (GTK_BIN (self)); + if (child != NULL) + gtk_widget_get_preferred_height (child, min_height, nat_height); + + style_context = gtk_widget_get_style_context (widget); + dzl_gtk_style_context_get_borders (style_context, &borders); + + *min_height += (borders.top + borders.bottom); + *nat_height += (borders.top + borders.bottom); +} + +static void +dzl_bin_class_init (DzlBinClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + widget_class->draw = dzl_bin_draw; + widget_class->get_preferred_width = dzl_bin_get_preferred_width; + widget_class->get_preferred_height = dzl_bin_get_preferred_height; + widget_class->size_allocate = dzl_bin_size_allocate; + + gtk_widget_class_set_css_name (widget_class, "dzlbin"); +} + +static void +dzl_bin_init (DzlBin *self) +{ +} + +GtkWidget * +dzl_bin_new (void) +{ + return g_object_new (DZL_TYPE_BIN, NULL); +} diff --git a/src/widgets/dzl-bin.h b/src/widgets/dzl-bin.h new file mode 100644 index 0000000..b5b91c5 --- /dev/null +++ b/src/widgets/dzl-bin.h @@ -0,0 +1,47 @@ +/* dzl-bin.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_BIN_H +#define DZL_BIN_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_BIN (dzl_bin_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlBin, dzl_bin, DZL, BIN, GtkBin) + +struct _DzlBinClass +{ + GtkBinClass parent_class; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_bin_new (void); + +G_END_DECLS + +#endif /* DZL_BIN_H */ diff --git a/src/widgets/dzl-bolding-label.c b/src/widgets/dzl-bolding-label.c new file mode 100644 index 0000000..2dc41b3 --- /dev/null +++ b/src/widgets/dzl-bolding-label.c @@ -0,0 +1,189 @@ +/* dzl-bolding-label.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#include "config.h" + +#include + +#include "dzl-bolding-label.h" + +/** + * SECTION:dzl-bolding-label + * + * This is a GtkLabel widget that will allocate extra space if necessary + * so that the size request will not change when the contents of the + * label are bolded. + * + * This might be useful when you want to change a label based on some + * selection state without it affecting the size request or layout. + */ + +struct _DzlBoldingLabel +{ + GtkLabel parent_instance; +}; + +G_DEFINE_TYPE (DzlBoldingLabel, dzl_bolding_label, GTK_TYPE_LABEL) + +enum { + PROP_0, + PROP_BOLD, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_bolding_label_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + const PangoFontDescription *font_desc; + PangoContext *context; + + g_assert (DZL_IS_BOLDING_LABEL (widget)); + g_assert (min_width); + g_assert (nat_width); + + GTK_WIDGET_CLASS (dzl_bolding_label_parent_class)->get_preferred_width (widget, min_width, nat_width); + + if (NULL == (context = gtk_widget_get_pango_context (widget)) || + NULL == (font_desc = pango_context_get_font_description (context))) + return; + + if (pango_font_description_get_weight (font_desc) != PANGO_WEIGHT_BOLD) + { + PangoFontDescription *font_desc_copy; + PangoLayout *layout; + const gchar *text; + PangoEllipsizeMode ellipsize; + gint height; + gint width; + + text = gtk_label_get_text (GTK_LABEL (widget)); + layout = gtk_widget_create_pango_layout (widget, text); + font_desc_copy = pango_font_description_copy (font_desc); + ellipsize = gtk_label_get_ellipsize (GTK_LABEL (widget)); + + pango_font_description_set_weight (font_desc_copy, PANGO_WEIGHT_BOLD); + pango_layout_set_font_description (layout, font_desc_copy); + pango_layout_set_ellipsize (layout, ellipsize); + pango_layout_get_pixel_size (layout, &width, &height); + + if (ellipsize == PANGO_ELLIPSIZE_NONE) + { + /* + * Only apply min_width if we cannot ellipsize, if we can, that + * effects things differently. + */ + if (width > *min_width) + *min_width = width; + } + + if (width > *nat_width) + *nat_width = width; + + pango_font_description_free (font_desc_copy); + g_object_unref (layout); + } +} + +static void +dzl_bolding_label_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlBoldingLabel *self = DZL_BOLDING_LABEL (object); + + switch (prop_id) + { + case PROP_BOLD: + dzl_bolding_label_set_bold (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_bolding_label_class_init (DzlBoldingLabelClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = dzl_bolding_label_set_property; + + widget_class->get_preferred_width = dzl_bolding_label_get_preferred_width; + + properties [PROP_BOLD] = + g_param_spec_boolean ("bold", + "Bold", + "Set the bold weight for the label", + FALSE, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_bolding_label_init (DzlBoldingLabel *self) +{ +} + +static gboolean +remove_weights (PangoAttribute *attr, + gpointer user_data) +{ + return attr->klass->type == ((PangoAttribute *)user_data)->klass->type; +} + +void +dzl_bolding_label_set_weight (DzlBoldingLabel *self, + PangoWeight weight) +{ + PangoAttrList *filtered; + PangoAttrList *attrs; + PangoAttrList *copy; + PangoAttribute *attr; + + g_return_if_fail (DZL_IS_BOLDING_LABEL (self)); + + attrs = gtk_label_get_attributes (GTK_LABEL (self)); + if (attrs) + copy = pango_attr_list_copy (attrs); + else + copy = pango_attr_list_new (); + attr = pango_attr_weight_new (weight); + filtered = pango_attr_list_filter (copy, remove_weights, attr); + pango_attr_list_insert (copy, attr); + gtk_label_set_attributes (GTK_LABEL (self), copy); + gtk_widget_queue_draw (GTK_WIDGET (self)); + pango_attr_list_unref (filtered); + pango_attr_list_unref (copy); +} + +void +dzl_bolding_label_set_bold (DzlBoldingLabel *self, + gboolean bold) +{ + g_return_if_fail (DZL_IS_BOLDING_LABEL (self)); + + dzl_bolding_label_set_weight (self, bold ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL); +} diff --git a/src/widgets/dzl-bolding-label.h b/src/widgets/dzl-bolding-label.h new file mode 100644 index 0000000..d0e7604 --- /dev/null +++ b/src/widgets/dzl-bolding-label.h @@ -0,0 +1,42 @@ +/* dzl-bolding-label.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_BOLDING_LABEL_H +#define DZL_BOLDING_LABEL_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_BOLDING_LABEL (dzl_bolding_label_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlBoldingLabel, dzl_bolding_label, DZL, BOLDING_LABEL, GtkLabel) + +DZL_AVAILABLE_IN_ALL +void dzl_bolding_label_set_weight (DzlBoldingLabel *self, + PangoWeight weight); +DZL_AVAILABLE_IN_ALL +void dzl_bolding_label_set_bold (DzlBoldingLabel *self, + gboolean bold); + +G_END_DECLS + +#endif /* DZL_BOLDING_LABEL_H */ diff --git a/src/widgets/dzl-box.c b/src/widgets/dzl-box.c new file mode 100644 index 0000000..3a4027b --- /dev/null +++ b/src/widgets/dzl-box.c @@ -0,0 +1,157 @@ +/* dzl-box.c + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#include "config.h" + +#include + +#include "dzl-box.h" + +typedef struct +{ + gint max_width_request; +} DzlBoxPrivate; + +enum { + PROP_0, + PROP_MAX_WIDTH_REQUEST, + LAST_PROP +}; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlBox, dzl_box, GTK_TYPE_BOX) + +static GParamSpec *properties [LAST_PROP]; + +/** + * dzl_box_get_nth_child: + * @self: a #DzlBox + * @nth: the index of the child starting from 0 + * + * Gets the nth child of @self. + * + * Returns: (transfer none) (nullable): A #GtkWidget or %NULL + */ +GtkWidget * +dzl_box_get_nth_child (DzlBox *self, + guint nth) +{ + GtkWidget *ret = NULL; + GList *list; + + g_return_val_if_fail (GTK_IS_BOX (self), NULL); + + list = gtk_container_get_children (GTK_CONTAINER (self)); + ret = g_list_nth_data (list, nth); + g_list_free (list); + + return ret; +} + +static void +dzl_box_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + DzlBox *self = (DzlBox *)widget; + DzlBoxPrivate *priv = dzl_box_get_instance_private (self); + + g_assert (DZL_IS_BOX (self)); + + GTK_WIDGET_CLASS (dzl_box_parent_class)->get_preferred_width (widget, min_width, nat_width); + + if (priv->max_width_request > 0) + { + if (*min_width > priv->max_width_request) + *min_width = priv->max_width_request; + + if (*nat_width > priv->max_width_request) + *nat_width = priv->max_width_request; + } +} + +static void +dzl_box_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlBox *self = DZL_BOX (object); + DzlBoxPrivate *priv = dzl_box_get_instance_private (self); + + switch (prop_id) + { + case PROP_MAX_WIDTH_REQUEST: + g_value_set_int (value, priv->max_width_request); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_box_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlBox *self = DZL_BOX (object); + DzlBoxPrivate *priv = dzl_box_get_instance_private (self); + + switch (prop_id) + { + case PROP_MAX_WIDTH_REQUEST: + priv->max_width_request = g_value_get_int (value); + gtk_widget_queue_resize (GTK_WIDGET (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_box_class_init (DzlBoxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = dzl_box_get_property; + object_class->set_property = dzl_box_set_property; + + widget_class->get_preferred_width = dzl_box_get_preferred_width; + + properties [PROP_MAX_WIDTH_REQUEST] = + g_param_spec_int ("max-width-request", + "Max Width Request", + "Max Width Request", + -1, + G_MAXINT, + -1, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_box_init (DzlBox *self) +{ + DzlBoxPrivate *priv = dzl_box_get_instance_private (self); + + priv->max_width_request = -1; +} diff --git a/src/widgets/dzl-box.h b/src/widgets/dzl-box.h new file mode 100644 index 0000000..22e71c0 --- /dev/null +++ b/src/widgets/dzl-box.h @@ -0,0 +1,51 @@ +/* dzl-box.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_BOX_H +#define DZL_BOX_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_BOX (dzl_box_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlBox, dzl_box, DZL, BOX, GtkBox) + +struct _DzlBoxClass +{ + GtkBoxClass parent_class; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_box_new (void); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_box_get_nth_child (DzlBox *self, + guint nth); +DZL_AVAILABLE_IN_ALL +gint dzl_box_get_max_width_request (DzlBox *self); +DZL_AVAILABLE_IN_ALL +void dzl_box_set_max_width_request (DzlBox *self, + gint max_width_request); + +G_END_DECLS + +#endif /* DZL_BOX_H */ diff --git a/src/widgets/dzl-centering-bin.c b/src/widgets/dzl-centering-bin.c new file mode 100644 index 0000000..cda12ba --- /dev/null +++ b/src/widgets/dzl-centering-bin.c @@ -0,0 +1,326 @@ +/* dzl-centering-bin.c + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-centering-bin" + +#include "config.h" + +#include "widgets/dzl-centering-bin.h" +#include "bindings/dzl-signal-group.h" + +/** + * SECTION:dzlcenteringbin: + * @title: DzlCenteringBin + * @short_description: center a widget with respect to the toplevel + * + * First off, you probably want to use GtkBox with a center widget instead + * of this widget. However, the case where this widget is useful is when + * you cannot control your layout within the width of the toplevel, but + * still want your child centered within the toplevel. + * + * This is done by translating coordinates of the widget with respect to + * the toplevel and anchoring the child at TRUE_CENTER-(alloc.width/2). + */ + +typedef struct +{ + DzlSignalGroup *signals; + gint max_width_request; +} DzlCenteringBinPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlCenteringBin, dzl_centering_bin, GTK_TYPE_BIN) + +enum { + PROP_0, + PROP_MAX_WIDTH_REQUEST, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +GtkWidget * +dzl_centering_bin_new (void) +{ + return g_object_new (DZL_TYPE_CENTERING_BIN, NULL); +} + +static void +dzl_centering_bin_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + DzlCenteringBin *self = (DzlCenteringBin *)widget; + DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self); + GtkWidget *child; + + g_assert (GTK_IS_WIDGET (widget)); + g_assert (allocation != NULL); + + gtk_widget_set_allocation (widget, allocation); + + child = gtk_bin_get_child (GTK_BIN (widget)); + + if ((child != NULL) && gtk_widget_get_visible (child)) + { + GtkWidget *toplevel = gtk_widget_get_toplevel (child); + GtkAllocation top_allocation; + GtkAllocation child_allocation; + GtkRequisition nat_child_req; + gint translated_x; + gint translated_y; + gint border_width; + + border_width = gtk_container_get_border_width (GTK_CONTAINER (self)); + + gtk_widget_get_allocation (toplevel, &top_allocation); + gtk_widget_translate_coordinates (toplevel, widget, + top_allocation.x + (top_allocation.width / 2), + 0, + &translated_x, + &translated_y); + + gtk_widget_get_preferred_size (child, NULL, &nat_child_req); + + child_allocation.x = allocation->x; + child_allocation.y = allocation->y; + child_allocation.height = allocation->height; + child_allocation.width = translated_x * 2; + + child_allocation.y += border_width; + child_allocation.height -= border_width * 2; + + if (nat_child_req.width > child_allocation.width) + child_allocation.width = MIN (nat_child_req.width, allocation->width); + + if ((priv->max_width_request > 0) && (child_allocation.width > priv->max_width_request)) + { + child_allocation.x += (child_allocation.width - priv->max_width_request) / 2; + child_allocation.width = priv->max_width_request; + } + + gtk_widget_size_allocate (child, &child_allocation); + } +} + +static gboolean +queue_allocate_in_idle (gpointer data) +{ + g_autoptr(DzlCenteringBin) self = data; + + gtk_widget_queue_allocate (GTK_WIDGET (self)); + + return G_SOURCE_REMOVE; +} + +static void +dzl_centering_bin_toplevel_size_allocate (DzlCenteringBin *self, + GtkAllocation *allocation, + GtkWindow *toplevel) +{ + g_assert (DZL_IS_CENTERING_BIN (self)); + g_assert (GTK_IS_WINDOW (toplevel)); + + /* + * If we ::queue_allocate() immediately, we can get into a state where an + * allocation is needed when ::draw() should be called. That causes a + * warning on the command line, so instead just delay until we leave + * our current main loop iteration. + */ + g_timeout_add (0, queue_allocate_in_idle, g_object_ref (self)); +} + +static void +dzl_centering_bin_hierarchy_changed (GtkWidget *widget, + GtkWidget *previous_toplevel) +{ + DzlCenteringBin *self = (DzlCenteringBin *)widget; + DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self); + GtkWidget *toplevel; + + g_assert (DZL_IS_CENTERING_BIN (self)); + + /* + * The hierarcy has changed, so we need to ensure we get allocation change + * from the toplevel so we can relayout our child to be centered. + */ + + toplevel = gtk_widget_get_toplevel (widget); + + if (GTK_IS_WINDOW (toplevel)) + dzl_signal_group_set_target (priv->signals, toplevel); + else + dzl_signal_group_set_target (priv->signals, NULL); +} + +static void +dzl_centering_bin_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + DzlCenteringBin *self = (DzlCenteringBin *)widget; + DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self); + + g_assert (DZL_IS_CENTERING_BIN (self)); + + GTK_WIDGET_CLASS (dzl_centering_bin_parent_class)->get_preferred_width (widget, min_width, nat_width); + + if ((priv->max_width_request > 0) && (*min_width > priv->max_width_request)) + *min_width = priv->max_width_request; + + if ((priv->max_width_request > 0) && (*nat_width > priv->max_width_request)) + *nat_width = priv->max_width_request; +} + +static void +dzl_centering_bin_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *min_height, + gint *nat_height) +{ + DzlCenteringBin *self = (DzlCenteringBin *)widget; + DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self); + GtkWidget *child; + gint border_width; + + g_assert (DZL_IS_CENTERING_BIN (self)); + + /* + * TODO: Something is still not right here. I'm seeing a situation where + * we are not getting the proper height for width. See the extensions + * page in preferences. We are not getting the right height from + * our box child, due to GtkLabel line wrapping. + */ + + *min_height = 0; + *nat_height = 0; + + child = gtk_bin_get_child (GTK_BIN (self)); + if (child == NULL) + return; + + if ((priv->max_width_request) > 0 && (width > priv->max_width_request)) + width = priv->max_width_request; + + border_width = gtk_container_get_border_width (GTK_CONTAINER (widget)); + width -= border_width * 2; + + gtk_widget_get_preferred_height_for_width (child, width, min_height, nat_height); + + *min_height += border_width * 2; + *nat_height += border_width * 2; +} + +static GtkSizeRequestMode +dzl_centering_bin_get_request_mode (GtkWidget *widget) +{ + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + +static void +dzl_centering_bin_finalize (GObject *object) +{ + DzlCenteringBin *self = (DzlCenteringBin *)object; + DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self); + + g_clear_object (&priv->signals); + + G_OBJECT_CLASS (dzl_centering_bin_parent_class)->finalize (object); +} + +static void +dzl_centering_bin_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlCenteringBin *self = DZL_CENTERING_BIN(object); + DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self); + + switch (prop_id) + { + case PROP_MAX_WIDTH_REQUEST: + g_value_set_int (value, priv->max_width_request); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + } +} + +static void +dzl_centering_bin_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlCenteringBin *self = DZL_CENTERING_BIN(object); + DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self); + + switch (prop_id) + { + case PROP_MAX_WIDTH_REQUEST: + priv->max_width_request = g_value_get_int (value); + gtk_widget_queue_resize (GTK_WIDGET (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + } +} + +static void +dzl_centering_bin_class_init (DzlCenteringBinClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dzl_centering_bin_finalize; + object_class->get_property = dzl_centering_bin_get_property; + object_class->set_property = dzl_centering_bin_set_property; + + widget_class->get_preferred_height_for_width = dzl_centering_bin_get_preferred_height_for_width; + widget_class->get_preferred_width = dzl_centering_bin_get_preferred_width; + widget_class->get_request_mode = dzl_centering_bin_get_request_mode; + widget_class->hierarchy_changed = dzl_centering_bin_hierarchy_changed; + widget_class->size_allocate = dzl_centering_bin_size_allocate; + + properties [PROP_MAX_WIDTH_REQUEST] = + g_param_spec_int ("max-width-request", + "Max Width Request", + "Max Width Request", + -1, + G_MAXINT, + -1, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_centering_bin_init (DzlCenteringBin *self) +{ + DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self); + + priv->signals = dzl_signal_group_new (GTK_TYPE_WINDOW); + priv->max_width_request = -1; + + dzl_signal_group_connect_object (priv->signals, + "size-allocate", + G_CALLBACK (dzl_centering_bin_toplevel_size_allocate), + self, + G_CONNECT_SWAPPED); +} diff --git a/src/widgets/dzl-centering-bin.h b/src/widgets/dzl-centering-bin.h new file mode 100644 index 0000000..1cac70f --- /dev/null +++ b/src/widgets/dzl-centering-bin.h @@ -0,0 +1,43 @@ +/* dzl-centering-bin.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_CENTERING_BIN_H +#define DZL_CENTERING_BIN_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_CENTERING_BIN (dzl_centering_bin_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlCenteringBin, dzl_centering_bin, DZL, CENTERING_BIN, GtkBin) + +struct _DzlCenteringBinClass +{ + GtkBinClass parent; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_centering_bin_new (void); + +G_END_DECLS + +#endif /* DZL_CENTERING_BIN_H */ diff --git a/src/widgets/dzl-column-layout.c b/src/widgets/dzl-column-layout.c new file mode 100644 index 0000000..05fa1c5 --- /dev/null +++ b/src/widgets/dzl-column-layout.c @@ -0,0 +1,712 @@ +/* dzl-column-layout.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#include "config.h" + +#include "dzl-column-layout.h" + +typedef struct +{ + GtkWidget *widget; + GtkAllocation alloc; + GtkRequisition req; + GtkRequisition min_req; + gint priority; +} DzlColumnLayoutChild; + +typedef struct +{ + GArray *children; + gint column_width; + gint column_spacing; + gint row_spacing; + guint max_columns; +} DzlColumnLayoutPrivate; + +#define COLUMN_WIDTH_DEFAULT 500 +#define COLUMN_SPACING_DEFAULT 24 +#define ROW_SPACING_DEFAULT 24 + +G_DEFINE_TYPE_WITH_PRIVATE (DzlColumnLayout, dzl_column_layout, GTK_TYPE_CONTAINER) + +enum { + PROP_0, + PROP_COLUMN_WIDTH, + PROP_COLUMN_SPACING, + PROP_MAX_COLUMNS, + PROP_ROW_SPACING, + LAST_PROP +}; + +enum { + CHILD_PROP_0, + CHILD_PROP_PRIORITY, + LAST_CHILD_PROP +}; + +static GParamSpec *properties [LAST_PROP]; +static GParamSpec *child_properties [LAST_CHILD_PROP]; + +static void +dzl_column_layout_layout (DzlColumnLayout *self, + gint width, + gint height, + gint *tallest_column) +{ + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + gint real_tallest_column = 0; + gint total_height = 0; + gint n_columns = 0; + gint border_width; + gint column; + guint i; + + g_assert (DZL_IS_COLUMN_LAYOUT (self)); + g_assert (width > 0); + g_assert (tallest_column != NULL); + + /* + * We want to layout the children in a series of columns, but try to + * fill up each column before spilling into the next column. + * + * We can determine the number of columns we can support by the width + * of our allocation, and determine the max-height of each column + * by dividing the total height of all children by the number of + * columns. There is the chance that non-uniform sizing will mess up + * the height a bit here, but in practice it's mostly okay. + * + * The order of children is sorted by the priority, so that we know + * we can allocate them serially as we walk the array. + * + * We keep allocating children until we will go over the height of + * the column. + */ + + border_width = gtk_container_get_border_width (GTK_CONTAINER (self)); + total_height = border_width * 2; + + for (i = 0; i < priv->children->len; i++) + { + DzlColumnLayoutChild *child; + + child = &g_array_index (priv->children, DzlColumnLayoutChild, i); + + gtk_widget_get_preferred_height_for_width (child->widget, + priv->column_width, + &child->min_req.height, + &child->req.height); + + if (i != 0) + total_height += priv->row_spacing; + total_height += child->req.height; + } + + if (total_height <= height) + n_columns = 1; + else + n_columns = MAX (1, (width - (border_width * 2)) / (priv->column_width + priv->column_spacing)); + + if (priv->max_columns > 0) + n_columns = MIN (n_columns, (gint)priv->max_columns); + + for (column = 0, i = 0; column < n_columns; column++) + { + GtkAllocation alloc; + gint j = 0; + + alloc.x = border_width + (priv->column_width * column) + (column * priv->column_spacing); + alloc.y = border_width; + alloc.width = priv->column_width; + alloc.height = (height != 0) ? (height - (border_width * 2)) : total_height / n_columns; + + for (; i < priv->children->len; i++) + { + DzlColumnLayoutChild *child; + gint child_height; + + child = &g_array_index (priv->children, DzlColumnLayoutChild, i); + + /* + * Ignore this child if it is not visible. + */ + if (!gtk_widget_get_visible (child->widget) || + !gtk_widget_get_child_visible (child->widget)) + continue; + + /* + * If we are discovering height, and this is the last item in the + * first column, and we only have one column, then we will just + * make this "vexpand". + */ + if (priv->max_columns == 1 && i == priv->children->len - 1) + { + if (height == 0) + child_height = child->min_req.height; + else + child_height = alloc.height; + } + else + child_height = child->req.height; + + /* + * If the child requisition is taller than the space we have left in + * this column, we need to spill over to the next column. + */ + if ((j != 0) && (child_height > alloc.height) && (column < (n_columns - 1))) + break; + + child->alloc.x = alloc.x; + child->alloc.y = alloc.y; + child->alloc.width = priv->column_width; + child->alloc.height = child_height; + +#if 0 + g_print ("Allocating child to: [%d] %d,%d %dx%d\n", + column, + child->alloc.x, + child->alloc.y, + child->alloc.width, + child->alloc.height); +#endif + + alloc.y += child_height + priv->row_spacing; + alloc.height -= child_height + priv->row_spacing; + + if (alloc.y > real_tallest_column) + real_tallest_column = alloc.y; + + j++; + } + } + + real_tallest_column += border_width; + + *tallest_column = real_tallest_column; +} + +static GtkSizeRequestMode +dzl_column_layout_get_request_mode (GtkWidget *widget) +{ + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + +static void +dzl_column_layout_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + DzlColumnLayout *self = (DzlColumnLayout *)widget; + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + gint border_width; + gint n_columns = 3; + + g_assert (DZL_IS_COLUMN_LAYOUT (self)); + g_assert (min_width != NULL); + g_assert (nat_width != NULL); + + border_width = gtk_container_get_border_width (GTK_CONTAINER (self)); + + /* + * By default we try to natural size up to 3 columns. Otherwise, we + * use the max_columns. It would be nice if we could deal with this + * in a better way, but that is going to take a bunch more solving. + */ + + if (priv->max_columns > 0) + n_columns = priv->max_columns; + + *nat_width = (priv->column_width * n_columns) + (priv->column_spacing * (n_columns - 1)) + (border_width * 2); + *min_width = priv->column_width + (border_width * 2); +} + +static void +dzl_column_layout_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *min_height, + gint *nat_height) +{ + DzlColumnLayout *self = (DzlColumnLayout *)widget; + gint tallest_column = 0; + + g_assert (DZL_IS_COLUMN_LAYOUT (self)); + g_assert (min_height != NULL); + g_assert (nat_height != NULL); + + dzl_column_layout_layout (self, width, 0, &tallest_column); + + *min_height = *nat_height = tallest_column; +} + +static void +dzl_column_layout_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + DzlColumnLayout *self = (DzlColumnLayout *)widget; + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + gint tallest_column = 0; + guint i; + + g_assert (DZL_IS_COLUMN_LAYOUT (self)); + g_assert (allocation != NULL); + + gtk_widget_set_allocation (widget, allocation); + + dzl_column_layout_layout (self, allocation->width, allocation->height, &tallest_column); + + /* + * If we are on a RTL language, flip all our allocations around so + * we move from right to left. This is easier than adding all the + * complexity to directions during layout time. + */ + if (GTK_TEXT_DIR_RTL == gtk_widget_get_direction (widget)) + { + for (i = 0; i < priv->children->len; i++) + { + DzlColumnLayoutChild *child; + + child = &g_array_index (priv->children, DzlColumnLayoutChild, i); + child->alloc.x = allocation->x + allocation->width - child->alloc.x - child->alloc.width; + } + } + + for (i = 0; i < priv->children->len; i++) + { + DzlColumnLayoutChild *child; + + child = &g_array_index (priv->children, DzlColumnLayoutChild, i); + gtk_widget_size_allocate (child->widget, &child->alloc); + } +} + +static gint +dzl_column_layout_child_compare (gconstpointer a, + gconstpointer b) +{ + const DzlColumnLayoutChild *child_a = a; + const DzlColumnLayoutChild *child_b = b; + + return child_a->priority - child_b->priority; +} + +static void +dzl_column_layout_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlColumnLayout *self = (DzlColumnLayout *)container; + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + DzlColumnLayoutChild child = { 0 }; + + g_assert (DZL_IS_COLUMN_LAYOUT (self)); + g_assert (GTK_IS_WIDGET (widget)); + + child.widget = g_object_ref_sink (widget); + child.priority = 0; + + g_array_append_val (priv->children, child); + g_array_sort (priv->children, dzl_column_layout_child_compare); + + gtk_widget_set_parent (widget, GTK_WIDGET (self)); + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static void +dzl_column_layout_remove (GtkContainer *container, + GtkWidget *widget) +{ + DzlColumnLayout *self = (DzlColumnLayout *)container; + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + guint i; + + g_assert (GTK_IS_CONTAINER (container)); + g_assert (GTK_IS_WIDGET (widget)); + + for (i = 0; i < priv->children->len; i++) + { + DzlColumnLayoutChild *child; + + child = &g_array_index (priv->children, DzlColumnLayoutChild, i); + + if (child->widget == widget) + { + gtk_widget_unparent (child->widget); + g_array_remove_index (priv->children, i); + gtk_widget_queue_resize (GTK_WIDGET (self)); + return; + } + } +} + +static void +dzl_column_layout_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer user_data) +{ + DzlColumnLayout *self = (DzlColumnLayout *)container; + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + gint i; + + g_assert (GTK_IS_CONTAINER (container)); + g_assert (callback != NULL); + + /* + * We walk backwards in the array to be safe against callback destorying + * the widget (and causing it to be removed). + */ + + for (i = priv->children->len; i > 0; i--) + { + DzlColumnLayoutChild *child; + + child = &g_array_index (priv->children, DzlColumnLayoutChild, i - 1); + callback (child->widget, user_data); + } +} + +static DzlColumnLayoutChild * +dzl_column_layout_find_child (DzlColumnLayout *self, + GtkWidget *widget) +{ + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + + g_assert (DZL_IS_COLUMN_LAYOUT (self)); + g_assert (GTK_IS_WIDGET (widget)); + + for (guint i = 0; i < priv->children->len; i++) + { + DzlColumnLayoutChild *child; + + child = &g_array_index (priv->children, DzlColumnLayoutChild, i); + + if (child->widget == widget) + return child; + } + + g_assert_not_reached (); + + return NULL; +} + +gint +dzl_column_layout_get_column_width (DzlColumnLayout *self) +{ + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + g_return_val_if_fail (DZL_IS_COLUMN_LAYOUT (self), 0); + return priv->column_width; +} + +void +dzl_column_layout_set_column_width (DzlColumnLayout *self, + gint column_width) +{ + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + + g_return_if_fail (DZL_IS_COLUMN_LAYOUT (self)); + g_return_if_fail (column_width >= 0); + + if (priv->column_width != column_width) + { + priv->column_width = column_width; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COLUMN_WIDTH]); + } +} + +gint +dzl_column_layout_get_column_spacing (DzlColumnLayout *self) +{ + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + g_return_val_if_fail (DZL_IS_COLUMN_LAYOUT (self), 0); + return priv->column_spacing; +} + +void +dzl_column_layout_set_column_spacing (DzlColumnLayout *self, + gint column_spacing) +{ + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + + g_return_if_fail (DZL_IS_COLUMN_LAYOUT (self)); + g_return_if_fail (column_spacing >= 0); + + if (priv->column_spacing != column_spacing) + { + priv->column_spacing = column_spacing; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COLUMN_SPACING]); + } +} + +gint +dzl_column_layout_get_row_spacing (DzlColumnLayout *self) +{ + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + g_return_val_if_fail (DZL_IS_COLUMN_LAYOUT (self), 0); + return priv->row_spacing; +} + +void +dzl_column_layout_set_row_spacing (DzlColumnLayout *self, + gint row_spacing) +{ + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + + g_return_if_fail (DZL_IS_COLUMN_LAYOUT (self)); + g_return_if_fail (row_spacing >= 0); + + if (priv->row_spacing != row_spacing) + { + priv->row_spacing = row_spacing; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ROW_SPACING]); + } +} + +static void +dzl_column_layout_get_child_property (GtkContainer *container, + GtkWidget *widget, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlColumnLayout *self = (DzlColumnLayout *)container; + DzlColumnLayoutChild *child = dzl_column_layout_find_child (self, widget); + + switch (prop_id) + { + case CHILD_PROP_PRIORITY: + g_value_set_int (value, child->priority); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_column_layout_set_child_property (GtkContainer *container, + GtkWidget *widget, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlColumnLayout *self = (DzlColumnLayout *)container; + DzlColumnLayoutChild *child = dzl_column_layout_find_child (self, widget); + + switch (prop_id) + { + case CHILD_PROP_PRIORITY: + child->priority = g_value_get_int (value); + gtk_widget_queue_allocate (GTK_WIDGET (self)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_column_layout_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlColumnLayout *self = DZL_COLUMN_LAYOUT(object); + + switch (prop_id) + { + case PROP_COLUMN_SPACING: + g_value_set_int (value, dzl_column_layout_get_column_spacing (self)); + break; + + case PROP_COLUMN_WIDTH: + g_value_set_int (value, dzl_column_layout_get_column_width (self)); + break; + + case PROP_MAX_COLUMNS: + g_value_set_uint (value, dzl_column_layout_get_max_columns (self)); + break; + + case PROP_ROW_SPACING: + g_value_set_int (value, dzl_column_layout_get_row_spacing (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + } +} + +static void +dzl_column_layout_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlColumnLayout *self = DZL_COLUMN_LAYOUT(object); + + switch (prop_id) + { + case PROP_COLUMN_SPACING: + dzl_column_layout_set_column_spacing (self, g_value_get_int (value)); + break; + + case PROP_COLUMN_WIDTH: + dzl_column_layout_set_column_width (self, g_value_get_int (value)); + break; + + case PROP_MAX_COLUMNS: + dzl_column_layout_set_max_columns (self, g_value_get_uint (value)); + break; + + case PROP_ROW_SPACING: + dzl_column_layout_set_row_spacing (self, g_value_get_int (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + } +} + +static void +dzl_column_layout_finalize (GObject *object) +{ + DzlColumnLayout *self = (DzlColumnLayout *)object; + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + + g_clear_pointer (&priv->children, g_array_unref); + + G_OBJECT_CLASS (dzl_column_layout_parent_class)->finalize (object); +} + +static void +dzl_column_layout_class_init (DzlColumnLayoutClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->finalize = dzl_column_layout_finalize; + object_class->get_property = dzl_column_layout_get_property; + object_class->set_property = dzl_column_layout_set_property; + + properties [PROP_COLUMN_SPACING] = + g_param_spec_int ("column-spacing", + "Column Spacing", + "The spacing between columns", + 0, + G_MAXINT, + COLUMN_SPACING_DEFAULT, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_COLUMN_WIDTH] = + g_param_spec_int ("column-width", + "Column Width", + "The width of the columns", + 0, + G_MAXINT, + COLUMN_WIDTH_DEFAULT, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_MAX_COLUMNS] = + g_param_spec_uint ("max-columns", + "Max Columns", + "Max Columns", + 0, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_ROW_SPACING] = + g_param_spec_int ("row-spacing", + "Row Spacing", + "The spacing between rows", + 0, + G_MAXINT, + ROW_SPACING_DEFAULT, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + widget_class->get_preferred_height_for_width = dzl_column_layout_get_preferred_height_for_width; + widget_class->get_preferred_width = dzl_column_layout_get_preferred_width; + widget_class->get_request_mode = dzl_column_layout_get_request_mode; + widget_class->size_allocate = dzl_column_layout_size_allocate; + + container_class->add = dzl_column_layout_add; + container_class->forall = dzl_column_layout_forall; + container_class->remove = dzl_column_layout_remove; + container_class->get_child_property = dzl_column_layout_get_child_property; + container_class->set_child_property = dzl_column_layout_set_child_property; + + child_properties [CHILD_PROP_PRIORITY] = + g_param_spec_int ("priority", + "Priority", + "The sort priority of the child", + G_MININT, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + gtk_container_class_install_child_properties (container_class, LAST_CHILD_PROP, child_properties); +} + +static void +dzl_column_layout_init (DzlColumnLayout *self) +{ + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + + gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); + + priv->children = g_array_new (FALSE, TRUE, sizeof (DzlColumnLayoutChild)); + + priv->column_width = COLUMN_WIDTH_DEFAULT; + priv->column_spacing = COLUMN_SPACING_DEFAULT; + priv->row_spacing = ROW_SPACING_DEFAULT; +} + +GtkWidget * +dzl_column_layout_new (void) +{ + return g_object_new (DZL_TYPE_COLUMN_LAYOUT, NULL); +} + +guint +dzl_column_layout_get_max_columns (DzlColumnLayout *self) +{ + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_COLUMN_LAYOUT (self), 0); + + return priv->max_columns; +} + +void +dzl_column_layout_set_max_columns (DzlColumnLayout *self, + guint max_columns) +{ + DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self); + + g_return_if_fail (DZL_IS_COLUMN_LAYOUT (self)); + + if (priv->max_columns != max_columns) + { + priv->max_columns = max_columns; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MAX_COLUMNS]); + } +} diff --git a/src/widgets/dzl-column-layout.h b/src/widgets/dzl-column-layout.h new file mode 100644 index 0000000..05a076b --- /dev/null +++ b/src/widgets/dzl-column-layout.h @@ -0,0 +1,63 @@ +/* dzl-column-layout.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_COLUMN_LAYOUT_H +#define DZL_COLUMN_LAYOUT_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_COLUMN_LAYOUT (dzl_column_layout_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlColumnLayout, dzl_column_layout, DZL, COLUMN_LAYOUT, GtkContainer) + +struct _DzlColumnLayoutClass +{ + GtkContainerClass parent; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_column_layout_new (void); +DZL_AVAILABLE_IN_ALL +guint dzl_column_layout_get_max_columns (DzlColumnLayout *self); +DZL_AVAILABLE_IN_ALL +void dzl_column_layout_set_max_columns (DzlColumnLayout *self, + guint max_columns); +DZL_AVAILABLE_IN_ALL +gint dzl_column_layout_get_column_width (DzlColumnLayout *self); +DZL_AVAILABLE_IN_ALL +void dzl_column_layout_set_column_width (DzlColumnLayout *self, + gint column_width); +DZL_AVAILABLE_IN_ALL +gint dzl_column_layout_get_column_spacing (DzlColumnLayout *self); +DZL_AVAILABLE_IN_ALL +void dzl_column_layout_set_column_spacing (DzlColumnLayout *self, + gint column_spacing); +DZL_AVAILABLE_IN_ALL +gint dzl_column_layout_get_row_spacing (DzlColumnLayout *self); +DZL_AVAILABLE_IN_ALL +void dzl_column_layout_set_row_spacing (DzlColumnLayout *self, + gint row_spacing); + +G_END_DECLS + +#endif /* DZL_COLUMN_LAYOUT_H */ diff --git a/src/widgets/dzl-counters-window.c b/src/widgets/dzl-counters-window.c new file mode 100644 index 0000000..58b4ee4 --- /dev/null +++ b/src/widgets/dzl-counters-window.c @@ -0,0 +1,233 @@ +/* dzl-counters-window.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-counters-window" + +#include "config.h" + +#include "dzl-counters-window.h" + +typedef struct +{ + GtkTreeView *tree_view; + GtkTreeStore *tree_store; + GtkTreeViewColumn *value_column; + GtkCellRendererText *value_cell; + + DzlCounterArena *arena; + + guint update_source; +} DzlCountersWindowPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlCountersWindow, dzl_counters_window, GTK_TYPE_WINDOW) + +static void +get_value_cell_data_func (GtkCellLayout *cell_layout, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer user_data) +{ + static PangoAttrList *attrs; + DzlCounter *counter = NULL; + g_autofree gchar *str = NULL; + gint64 value = 0; + + if G_UNLIKELY (attrs == NULL) + { + attrs = pango_attr_list_new (); + pango_attr_list_insert (attrs, pango_attr_foreground_alpha_new (0.35 * G_MAXUSHORT)); + } + + gtk_tree_model_get (model, iter, 0, &counter, -1); + + if (counter != NULL) + value = dzl_counter_get (counter); + + str = g_strdup_printf ("%"G_GINT64_FORMAT, value); + + g_object_set (cell, + "attributes", value == 0 ? attrs : NULL, + "text", str, + NULL); +} + +static gboolean +update_display (gpointer data) +{ + DzlCountersWindow *self = data; + DzlCountersWindowPrivate *priv = dzl_counters_window_get_instance_private (self); + GdkWindow *window; + + g_assert (DZL_IS_COUNTERS_WINDOW (self)); + + window = gtk_tree_view_get_bin_window (priv->tree_view); + gdk_window_invalidate_rect (window, NULL, FALSE); + + return G_SOURCE_CONTINUE; +} + +static void +dzl_counters_window_realize (GtkWidget *widget) +{ + DzlCountersWindow *self = (DzlCountersWindow *)widget; + DzlCountersWindowPrivate *priv = dzl_counters_window_get_instance_private (self); + + g_assert (DZL_IS_COUNTERS_WINDOW (self)); + + priv->update_source = + g_timeout_add_seconds_full (G_PRIORITY_LOW, 1, update_display, self, NULL); + + GTK_WIDGET_CLASS (dzl_counters_window_parent_class)->realize (widget); +} + +static void +dzl_counters_window_unrealize (GtkWidget *widget) +{ + DzlCountersWindow *self = (DzlCountersWindow *)widget; + DzlCountersWindowPrivate *priv = dzl_counters_window_get_instance_private (self); + + g_assert (DZL_IS_COUNTERS_WINDOW (self)); + + if (priv->update_source != 0) + { + g_source_remove (priv->update_source); + priv->update_source = 0; + } + + GTK_WIDGET_CLASS (dzl_counters_window_parent_class)->unrealize (widget); +} + +static void +dzl_counters_window_finalize (GObject *object) +{ + DzlCountersWindow *self = (DzlCountersWindow *)object; + DzlCountersWindowPrivate *priv = dzl_counters_window_get_instance_private (self); + + g_clear_pointer (&priv->arena, dzl_counter_arena_unref); + g_clear_object (&priv->tree_store); + + G_OBJECT_CLASS (dzl_counters_window_parent_class)->finalize (object); +} + +static void +dzl_counters_window_class_init (DzlCountersWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_counters_window_finalize; + + widget_class->realize = dzl_counters_window_realize; + widget_class->unrealize = dzl_counters_window_unrealize; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-counters-window.ui"); + gtk_widget_class_bind_template_child_private (widget_class, DzlCountersWindow, tree_view); + gtk_widget_class_bind_template_child_private (widget_class, DzlCountersWindow, value_cell); + gtk_widget_class_bind_template_child_private (widget_class, DzlCountersWindow, value_column); +} + +static void +dzl_counters_window_init (DzlCountersWindow *self) +{ + DzlCountersWindowPrivate *priv = dzl_counters_window_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); + + priv->tree_store = gtk_tree_store_new (4, G_TYPE_POINTER, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); + gtk_tree_view_set_model (priv->tree_view, GTK_TREE_MODEL (priv->tree_store)); + + gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (priv->value_column), + GTK_CELL_RENDERER (priv->value_cell), + get_value_cell_data_func, + NULL, NULL); +} + +static void +foreach_counter_cb (DzlCounter *counter, + gpointer user_data) +{ + DzlCountersWindow *self = user_data; + DzlCountersWindowPrivate *priv = dzl_counters_window_get_instance_private (self); + GtkTreeIter iter; + + g_assert (DZL_IS_COUNTERS_WINDOW (self)); + + gtk_tree_store_append (priv->tree_store, &iter, NULL); + gtk_tree_store_set (priv->tree_store, &iter, + 0, counter, + 1, counter->category, + 2, counter->name, + 3, counter->description, + -1); +} + +static void +dzl_counters_window_reload (DzlCountersWindow *self) +{ + DzlCountersWindowPrivate *priv = dzl_counters_window_get_instance_private (self); + + g_assert (DZL_IS_COUNTERS_WINDOW (self)); + + gtk_tree_store_clear (priv->tree_store); + if (priv->arena == NULL) + return; + + dzl_counter_arena_foreach (priv->arena, foreach_counter_cb, self); +} + +GtkWidget * +dzl_counters_window_new (void) +{ + return g_object_new (DZL_TYPE_COUNTERS_WINDOW, NULL); +} + +/** + * dzl_counters_window_get_arena: + * @self: a #DzlCountersWindow + * + * Gets the currently viewed arena, if any. + * + * Returns: (transfer none) (nullable): A #DzlCounterArena or %NULL. + */ +DzlCounterArena * +dzl_counters_window_get_arena (DzlCountersWindow *self) +{ + DzlCountersWindowPrivate *priv = dzl_counters_window_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_COUNTERS_WINDOW (self), NULL); + + return priv->arena; +} + +void +dzl_counters_window_set_arena (DzlCountersWindow *self, + DzlCounterArena *arena) +{ + DzlCountersWindowPrivate *priv = dzl_counters_window_get_instance_private (self); + + g_return_if_fail (DZL_IS_COUNTERS_WINDOW (self)); + + if (arena != priv->arena) + { + g_clear_pointer (&priv->arena, dzl_counter_arena_unref); + if (arena != NULL) + priv->arena = dzl_counter_arena_ref (arena); + dzl_counters_window_reload (self); + } +} diff --git a/src/widgets/dzl-counters-window.h b/src/widgets/dzl-counters-window.h new file mode 100644 index 0000000..86694ad --- /dev/null +++ b/src/widgets/dzl-counters-window.h @@ -0,0 +1,55 @@ +/* dzl-counters-window.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_COUNTERS_WINDOW_H +#define DZL_COUNTERS_WINDOW_H + +#include + +#include "dzl-version-macros.h" + +#include "util/dzl-counter.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_COUNTERS_WINDOW (dzl_counters_window_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlCountersWindow, dzl_counters_window, DZL, COUNTERS_WINDOW, GtkWindow) + +struct _DzlCountersWindowClass +{ + GtkWindowClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_counters_window_new (void); +DZL_AVAILABLE_IN_ALL +DzlCounterArena *dzl_counters_window_get_arena (DzlCountersWindow *self); +DZL_AVAILABLE_IN_ALL +void dzl_counters_window_set_arena (DzlCountersWindow *self, + DzlCounterArena *arena); + +G_END_DECLS + +#endif /* DZL_COUNTERS_WINDOW_H */ diff --git a/src/widgets/dzl-counters-window.ui b/src/widgets/dzl-counters-window.ui new file mode 100644 index 0000000..c936bf6 --- /dev/null +++ b/src/widgets/dzl-counters-window.ui @@ -0,0 +1,63 @@ + + + + diff --git a/src/widgets/dzl-elastic-bin.c b/src/widgets/dzl-elastic-bin.c new file mode 100644 index 0000000..5cca378 --- /dev/null +++ b/src/widgets/dzl-elastic-bin.c @@ -0,0 +1,280 @@ +/* dzl-elastic-bin.c + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-elastic-bin" + +#include "config.h" + +#include "animation/dzl-animation.h" +#include "widgets/dzl-elastic-bin.h" +#include "util/dzl-macros.h" + +#if 0 +# define _TRACE_LEVEL (1<hanim; + dzl_clear_weak_pointer (&priv->hanim); + if (anim != NULL) + dzl_animation_stop (anim); + + EXIT; +} + +static guint +dzl_elastic_bin_calculate_duration (DzlElasticBin *self, + gdouble from_value, + gdouble to_value) +{ + GdkDisplay *display; + GdkMonitor *monitor; + GdkWindow *window; + + g_assert (DZL_IS_ELASTIC_BIN (self)); + g_assert (from_value >= 0.0); + g_assert (to_value >= 0.0); + + if (NULL != (display = gtk_widget_get_display (GTK_WIDGET (self))) && + NULL != (window = gtk_widget_get_window (GTK_WIDGET (self))) && + NULL != (monitor = gdk_display_get_monitor_at_window (display, window))) + return dzl_animation_calculate_duration (monitor, from_value, to_value); + + return 0; +} + +static void +dzl_elastic_bin_animate_to (DzlElasticBin *self, + gdouble value) +{ + DzlElasticBinPrivate *priv = dzl_elastic_bin_get_instance_private (self); + DzlAnimation *anim; + guint duration; + + ENTRY; + + g_assert (DZL_IS_ELASTIC_BIN (self)); + + dzl_elastic_bin_cancel_animation (self); + + duration = dzl_elastic_bin_calculate_duration (self, + gtk_adjustment_get_value (priv->hadj), + value); + + if (duration == 0) + { + gtk_adjustment_set_value (priv->hadj, value); + EXIT; + } + + TRACE_MSG ("Duration is %u milliseconds", duration); + + anim = dzl_object_animate (priv->hadj, + DZL_ANIMATION_EASE_OUT_CUBIC, + duration, + gtk_widget_get_frame_clock (GTK_WIDGET (self)), + "value", value, + NULL); + dzl_set_weak_pointer (&priv->hanim, anim); + + EXIT; +} + +static void +dzl_elastic_bin_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *min_height, + gint *nat_height) +{ + DzlElasticBin *self = (DzlElasticBin *)widget; + DzlElasticBinPrivate *priv = dzl_elastic_bin_get_instance_private (self); + + ENTRY; + + TRACE_MSG ("width=%d", width); + + g_assert (DZL_IS_ELASTIC_BIN (self)); + + /* + * We must always chain up to the parent get_preferred_height_for_width() + * so that we can detect changes while we are animating. + */ + + GTK_WIDGET_CLASS (dzl_elastic_bin_parent_class)->get_preferred_height_for_width (widget, width, min_height, nat_height); + + /* + * If we are animating the widget, and the size request hasn't changed since + * our last animation frame, go ahead and process that now. + */ + + if (*min_height == priv->cached_min_height && + *nat_height == priv->cached_nat_height && + priv->hanim != NULL) + { + *min_height = priv->cached_min_height; + *nat_height = (gint)gtk_adjustment_get_value (priv->hadj); + + TRACE_MSG ("Fast path min=%d nat=%d", *min_height, *nat_height); + + if (*nat_height == priv->cached_nat_height) + dzl_elastic_bin_cancel_animation (self); + + EXIT; + } + + if (*min_height != priv->cached_min_height || *nat_height != priv->cached_nat_height) + { + priv->cached_min_height = *min_height; + priv->cached_nat_height = *nat_height; + + TRACE_MSG ("New requested height is min=%d nat=%d", + *min_height, *nat_height); + + if (*min_height > (gint)gtk_adjustment_get_value (priv->hadj)) + gtk_adjustment_set_value (priv->hadj, *min_height); + + *min_height = priv->cached_min_height; + *nat_height = (gint)gtk_adjustment_get_value (priv->hadj); + + dzl_elastic_bin_animate_to (self, priv->cached_nat_height); + + TRACE_MSG ("!!! min=%d nat=%d", *min_height, *nat_height); + + EXIT; + } + + TRACE_MSG ("*** min=%d nat=%d", *min_height, *nat_height); + + EXIT; +} + +static void +dzl_elastic_bin_hadj_value_changed (DzlElasticBin *self, + GtkAdjustment *adj) +{ + ENTRY; + + g_assert (DZL_IS_ELASTIC_BIN (self)); + g_assert (GTK_IS_ADJUSTMENT (adj)); + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + EXIT; +} + +static void +dzl_elastic_bin_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + g_assert (GTK_IS_WIDGET (widget)); + g_assert (allocation != NULL); + + TRACE_MSG ("Allocating %d,%d %dx%d", + allocation->x, allocation->y, + allocation->width, allocation->height); + + GTK_WIDGET_CLASS (dzl_elastic_bin_parent_class)->size_allocate (widget, allocation); +} + +static GtkSizeRequestMode +dzl_elastic_bin_get_request_mode (GtkWidget *widget) +{ + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + +static void +dzl_elastic_bin_destroy (GtkWidget *widget) +{ + DzlElasticBin *self = (DzlElasticBin *)widget; + + g_assert (DZL_IS_ELASTIC_BIN (self)); + + dzl_elastic_bin_cancel_animation (self); + + GTK_WIDGET_CLASS (dzl_elastic_bin_parent_class)->destroy (widget); +} + +static void +dzl_elastic_bin_finalize (GObject *object) +{ + DzlElasticBin *self = (DzlElasticBin *)object; + DzlElasticBinPrivate *priv = dzl_elastic_bin_get_instance_private (self); + + g_clear_object (&priv->hadj); + + G_OBJECT_CLASS (dzl_elastic_bin_parent_class)->finalize (object); +} + +static void +dzl_elastic_bin_class_init (DzlElasticBinClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_elastic_bin_finalize; + + widget_class->destroy = dzl_elastic_bin_destroy; + widget_class->get_preferred_height_for_width = dzl_elastic_bin_get_preferred_height_for_width; + widget_class->size_allocate = dzl_elastic_bin_size_allocate; + widget_class->get_request_mode = dzl_elastic_bin_get_request_mode; + + gtk_widget_class_set_css_name (widget_class, "elastic"); +} + +static void +dzl_elastic_bin_init (DzlElasticBin *self) +{ + DzlElasticBinPrivate *priv = dzl_elastic_bin_get_instance_private (self); + + priv->hadj = gtk_adjustment_new (0, 0, G_MAXINT, 1, 1, 1); + + g_signal_connect_object (priv->hadj, + "value-changed", + G_CALLBACK (dzl_elastic_bin_hadj_value_changed), + self, + G_CONNECT_SWAPPED); +} diff --git a/src/widgets/dzl-elastic-bin.h b/src/widgets/dzl-elastic-bin.h new file mode 100644 index 0000000..6edd256 --- /dev/null +++ b/src/widgets/dzl-elastic-bin.h @@ -0,0 +1,48 @@ +/* dzl-elastic-bin.h + * + * Copyright (C) 2017 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This file 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 Lesser 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 . + */ + +#ifndef DZL_ELASTIC_BIN_H +#define DZL_ELASTIC_BIN_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_ELASTIC_BIN (dzl_elastic_bin_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlElasticBin, dzl_elastic_bin, DZL, ELASTIC_BIN, GtkBin) + +struct _DzlElasticBinClass +{ + GtkBinClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_elastic_bin_new (void); + +G_END_DECLS + +#endif /* DZL_ELASTIC_BIN_H */ diff --git a/src/widgets/dzl-empty-state.c b/src/widgets/dzl-empty-state.c new file mode 100644 index 0000000..52e9c36 --- /dev/null +++ b/src/widgets/dzl-empty-state.c @@ -0,0 +1,408 @@ +/* dzl-empty-state.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#include "config.h" + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include + +#include "dzl-empty-state.h" + +typedef struct +{ + GtkBox *box; + GtkImage *image; + GtkLabel *subtitle; + GtkLabel *title; +} DzlEmptyStatePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlEmptyState, dzl_empty_state, GTK_TYPE_BIN) + +enum { + PROP_0, + PROP_ICON_NAME, + PROP_PIXEL_SIZE, + PROP_RESOURCE, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static gboolean +dzl_empty_state_action (GtkWidget *widget, + const gchar *prefix, + const gchar *action_name, + GVariant *parameter) +{ + GtkWidget *toplevel; + GApplication *app; + GActionGroup *group = NULL; + + g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE); + g_return_val_if_fail (prefix, FALSE); + g_return_val_if_fail (action_name, FALSE); + + app = g_application_get_default (); + toplevel = gtk_widget_get_toplevel (widget); + + while ((group == NULL) && (widget != NULL)) + { + group = gtk_widget_get_action_group (widget, prefix); + widget = gtk_widget_get_parent (widget); + } + + if (!group && g_str_equal (prefix, "win") && G_IS_ACTION_GROUP (toplevel)) + group = G_ACTION_GROUP (toplevel); + + if (!group && g_str_equal (prefix, "app") && G_IS_ACTION_GROUP (app)) + group = G_ACTION_GROUP (app); + + if (group && g_action_group_has_action (group, action_name)) + { + g_action_group_activate_action (group, action_name, parameter); + return TRUE; + } + + if (parameter && g_variant_is_floating (parameter)) + { + parameter = g_variant_ref_sink (parameter); + g_variant_unref (parameter); + } + + g_warning ("Failed to locate action %s.%s", prefix, action_name); + + return FALSE; +} + +static gboolean +dzl_empty_state_activate_link (DzlEmptyState *self, + const gchar *uri, + GtkLabel *label) +{ + g_assert (DZL_IS_EMPTY_STATE (self)); + g_assert (uri != NULL); + g_assert (GTK_IS_LABEL (label)); + + if (g_str_has_prefix (uri, "action://")) + { + g_autofree gchar *full_name = NULL; + g_autofree gchar *action_name = NULL; + g_autofree gchar *group_name = NULL; + g_autoptr(GVariant) param = NULL; + g_autoptr(GError) error = NULL; + + uri += strlen ("action://"); + + if (g_action_parse_detailed_name (uri, &full_name, ¶m, &error)) + { + const gchar *dot = strchr (full_name, '.'); + + if (param != NULL && g_variant_is_floating (param)) + param = g_variant_ref_sink (param); + + if (dot == NULL) + return FALSE; + + group_name = g_strndup (full_name, dot - full_name); + action_name = g_strdup (++dot); + + dzl_empty_state_action (GTK_WIDGET (self), + group_name, + action_name, + param); + + return TRUE; + } + else + g_warning ("%s", error->message); + } + + return FALSE; +} + +static void +dzl_empty_state_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlEmptyState *self = DZL_EMPTY_STATE (object); + DzlEmptyStatePrivate *priv = dzl_empty_state_get_instance_private (self); + + switch (prop_id) + { + case PROP_ICON_NAME: + g_value_set_string (value, dzl_empty_state_get_icon_name (self)); + break; + + case PROP_PIXEL_SIZE: + g_value_set_int (value, gtk_image_get_pixel_size (priv->image)); + break; + + case PROP_SUBTITLE: + g_value_set_string (value, dzl_empty_state_get_subtitle (self)); + break; + + case PROP_TITLE: + g_value_set_string (value, dzl_empty_state_get_title (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_empty_state_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlEmptyState *self = DZL_EMPTY_STATE (object); + DzlEmptyStatePrivate *priv = dzl_empty_state_get_instance_private (self); + + switch (prop_id) + { + case PROP_ICON_NAME: + dzl_empty_state_set_icon_name (self, g_value_get_string (value)); + break; + + case PROP_PIXEL_SIZE: + gtk_image_set_pixel_size (priv->image, g_value_get_int (value)); + break; + + case PROP_RESOURCE: + dzl_empty_state_set_resource (self, g_value_get_string (value)); + break; + + case PROP_SUBTITLE: + dzl_empty_state_set_subtitle (self, g_value_get_string (value)); + break; + + case PROP_TITLE: + dzl_empty_state_set_title (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_empty_state_class_init (DzlEmptyStateClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = dzl_empty_state_get_property; + object_class->set_property = dzl_empty_state_set_property; + + properties [PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "Icon Name", + "The name of the icon to display", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_PIXEL_SIZE] = + g_param_spec_int ("pixel-size", + "Pixel Size", + "Pixel Size", + 0, + G_MAXINT, + 128, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_RESOURCE] = + g_param_spec_string ("resource", + "Resource", + "A resource path to use for the icon", + NULL, + (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SUBTITLE] = + g_param_spec_string ("subtitle", + "Subtitle", + "The subtitle of the empty state", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "The title of the empty state", + NULL, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-empty-state.ui"); + gtk_widget_class_bind_template_child_private (widget_class, DzlEmptyState, box); + gtk_widget_class_bind_template_child_private (widget_class, DzlEmptyState, image); + gtk_widget_class_bind_template_child_private (widget_class, DzlEmptyState, title); + gtk_widget_class_bind_template_child_private (widget_class, DzlEmptyState, subtitle); +} + +static void +dzl_empty_state_init (DzlEmptyState *self) +{ + DzlEmptyStatePrivate *priv = dzl_empty_state_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); + + g_signal_connect_object (priv->subtitle, + "activate-link", + G_CALLBACK (dzl_empty_state_activate_link), + self, + G_CONNECT_SWAPPED); +} + +const gchar * +dzl_empty_state_get_icon_name (DzlEmptyState *self) +{ + DzlEmptyStatePrivate *priv = dzl_empty_state_get_instance_private (self); + const gchar *icon_name = NULL; + + g_return_val_if_fail (DZL_IS_EMPTY_STATE (self), NULL); + + gtk_image_get_icon_name (priv->image, &icon_name, NULL); + + return icon_name; +} + +void +dzl_empty_state_set_icon_name (DzlEmptyState *self, + const gchar *icon_name) +{ + DzlEmptyStatePrivate *priv = dzl_empty_state_get_instance_private (self); + + g_return_if_fail (DZL_IS_EMPTY_STATE (self)); + + if (g_strcmp0 (icon_name, dzl_empty_state_get_icon_name (self)) != 0) + { + GtkStyleContext *context; + + g_object_set (priv->image, + "icon-name", icon_name, + NULL); + + context = gtk_widget_get_style_context (GTK_WIDGET (priv->image)); + + if (icon_name != NULL && g_str_has_suffix (icon_name, "-symbolic")) + gtk_style_context_add_class (context, "dim-label"); + else + gtk_style_context_remove_class (context, "dim-label"); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]); + } +} + +const gchar * +dzl_empty_state_get_subtitle (DzlEmptyState *self) +{ + DzlEmptyStatePrivate *priv = dzl_empty_state_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_EMPTY_STATE (self), NULL); + + return gtk_label_get_label (priv->subtitle); +} + +void +dzl_empty_state_set_subtitle (DzlEmptyState *self, + const gchar *subtitle) +{ + DzlEmptyStatePrivate *priv = dzl_empty_state_get_instance_private (self); + + g_return_if_fail (DZL_IS_EMPTY_STATE (self)); + + if (g_strcmp0 (subtitle, dzl_empty_state_get_subtitle (self)) != 0) + { + gtk_label_set_label (priv->subtitle, subtitle); + gtk_widget_set_visible (GTK_WIDGET (priv->subtitle), subtitle && *subtitle); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SUBTITLE]); + } +} + +const gchar * +dzl_empty_state_get_title (DzlEmptyState *self) +{ + DzlEmptyStatePrivate *priv = dzl_empty_state_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_EMPTY_STATE (self), NULL); + + return gtk_label_get_label (priv->title); +} + +void +dzl_empty_state_set_title (DzlEmptyState *self, + const gchar *title) +{ + DzlEmptyStatePrivate *priv = dzl_empty_state_get_instance_private (self); + + g_return_if_fail (DZL_IS_EMPTY_STATE (self)); + + if (g_strcmp0 (title, dzl_empty_state_get_title (self)) != 0) + { + gtk_label_set_label (priv->title, title); + gtk_widget_set_visible (GTK_WIDGET (priv->title), title && *title); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]); + } +} + +void +dzl_empty_state_set_resource (DzlEmptyState *self, + const gchar *resource) +{ + DzlEmptyStatePrivate *priv = dzl_empty_state_get_instance_private (self); + + g_return_if_fail (DZL_IS_EMPTY_STATE (self)); + + if (resource != NULL) + { + GdkPixbuf *pixbuf; + GError *error = NULL; + gint scale_factor; + + scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self)); + + pixbuf = gdk_pixbuf_new_from_resource_at_scale (resource, + 128 * scale_factor, + 128 * scale_factor, + TRUE, + &error); + + if (pixbuf == NULL) + { + g_warning ("%s", error->message); + g_clear_error (&error); + return; + } + + g_object_set (priv->image, + "pixbuf", pixbuf, + NULL); + + g_clear_object (&pixbuf); + } +} diff --git a/src/widgets/dzl-empty-state.h b/src/widgets/dzl-empty-state.h new file mode 100644 index 0000000..9a3554b --- /dev/null +++ b/src/widgets/dzl-empty-state.h @@ -0,0 +1,61 @@ +/* dzl-empty-state.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_EMPTY_STATE_H +#define DZL_EMPTY_STATE_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_EMPTY_STATE (dzl_empty_state_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlEmptyState, dzl_empty_state, DZL, EMPTY_STATE, GtkBin) + +struct _DzlEmptyStateClass +{ + GtkBinClass parent_class; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_empty_state_new (void); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_empty_state_get_icon_name (DzlEmptyState *self); +DZL_AVAILABLE_IN_ALL +void dzl_empty_state_set_icon_name (DzlEmptyState *self, + const gchar *icon_name); +DZL_AVAILABLE_IN_ALL +void dzl_empty_state_set_resource (DzlEmptyState *self, + const gchar *resource); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_empty_state_get_title (DzlEmptyState *self); +DZL_AVAILABLE_IN_ALL +void dzl_empty_state_set_title (DzlEmptyState *self, + const gchar *title); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_empty_state_get_subtitle (DzlEmptyState *self); +DZL_AVAILABLE_IN_ALL +void dzl_empty_state_set_subtitle (DzlEmptyState *self, + const gchar *title); + +G_END_DECLS + +#endif /* DZL_EMPTY_STATE_H */ diff --git a/src/widgets/dzl-empty-state.ui b/src/widgets/dzl-empty-state.ui new file mode 100644 index 0000000..85332b5 --- /dev/null +++ b/src/widgets/dzl-empty-state.ui @@ -0,0 +1,43 @@ + + + + diff --git a/src/widgets/dzl-entry-box.c b/src/widgets/dzl-entry-box.c new file mode 100644 index 0000000..70cbd88 --- /dev/null +++ b/src/widgets/dzl-entry-box.c @@ -0,0 +1,149 @@ +/* dzl-entry-box.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#include "config.h" + +#include "dzl-entry-box.h" + +struct _DzlEntryBox +{ + GtkBox parent_instance; + + gint max_width_chars; +}; + +G_DEFINE_TYPE (DzlEntryBox, dzl_entry_box, GTK_TYPE_BOX) + +enum { + PROP_0, + PROP_MAX_WIDTH_CHARS, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_entry_box_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + DzlEntryBox *self = (DzlEntryBox *)widget; + + g_assert (DZL_IS_ENTRY_BOX (self)); + g_assert (min_width != NULL); + g_assert (nat_width != NULL); + + GTK_WIDGET_CLASS (dzl_entry_box_parent_class)->get_preferred_width (widget, min_width, nat_width); + + if (self->max_width_chars > 0) + { + PangoContext *context; + PangoFontMetrics *metrics; + gint char_width; + gint digit_width; + gint width; + + context = gtk_widget_get_pango_context (widget); + metrics = pango_context_get_metrics (context, + pango_context_get_font_description (context), + pango_context_get_language (context)); + + char_width = pango_font_metrics_get_approximate_char_width (metrics); + digit_width = pango_font_metrics_get_approximate_digit_width (metrics); + width = MAX (char_width, digit_width) * self->max_width_chars / PANGO_SCALE; + + if (width > *nat_width) + *nat_width = width; + + pango_font_metrics_unref (metrics); + } +} + +static void +dzl_entry_box_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlEntryBox *self = DZL_ENTRY_BOX (object); + + switch (prop_id) + { + case PROP_MAX_WIDTH_CHARS: + g_value_set_int (value, self->max_width_chars); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_entry_box_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlEntryBox *self = DZL_ENTRY_BOX (object); + + switch (prop_id) + { + case PROP_MAX_WIDTH_CHARS: + self->max_width_chars = g_value_get_int (value); + gtk_widget_queue_resize (GTK_WIDGET (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_entry_box_class_init (DzlEntryBoxClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = dzl_entry_box_get_property; + object_class->set_property = dzl_entry_box_set_property; + + widget_class->get_preferred_width = dzl_entry_box_get_preferred_width; + + properties [PROP_MAX_WIDTH_CHARS] = + g_param_spec_int ("max-width-chars", + "Max Width Chars", + "Max Width Chars", + -1, + G_MAXINT, + -1, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_css_name (widget_class, "entry"); +} + +static void +dzl_entry_box_init (DzlEntryBox *self) +{ + self->max_width_chars = -1; + + G_GNUC_BEGIN_IGNORE_DEPRECATIONS; + gtk_container_set_reallocate_redraws (GTK_CONTAINER (self), TRUE); + G_GNUC_END_IGNORE_DEPRECATIONS; +} diff --git a/src/widgets/dzl-entry-box.h b/src/widgets/dzl-entry-box.h new file mode 100644 index 0000000..a5e42dd --- /dev/null +++ b/src/widgets/dzl-entry-box.h @@ -0,0 +1,38 @@ +/* dzl-entry-box.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_ENTRY_BOX_H +#define DZL_ENTRY_BOX_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_ENTRY_BOX (dzl_entry_box_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlEntryBox, dzl_entry_box, DZL, ENTRY_BOX, GtkBox) + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_entry_box_new (void); + +G_END_DECLS + +#endif /* DZL_ENTRY_BOX_H */ diff --git a/src/widgets/dzl-file-chooser-entry.c b/src/widgets/dzl-file-chooser-entry.c new file mode 100644 index 0000000..9ff4fc4 --- /dev/null +++ b/src/widgets/dzl-file-chooser-entry.c @@ -0,0 +1,563 @@ +/* dzl-file-chooser-entry.c + * + * Copyright (C) 2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-file-chooser-entry" + +#include "config.h" + +#include + +#include "dzl-file-chooser-entry.h" + +typedef struct +{ + GtkEntry *entry; + GtkButton *button; + + GtkFileChooserDialog *dialog; + GtkFileFilter *filter; + GFile *file; + gchar *title; + + GtkFileChooserAction action; + + guint local_only : 1; + guint create_folders : 1; + guint do_overwrite_confirmation : 1; + guint select_multiple : 1; + guint show_hidden : 1; +} DzlFileChooserEntryPrivate; + +enum { + PROP_0, + PROP_ACTION, + PROP_CREATE_FOLDERS, + PROP_DO_OVERWRITE_CONFIRMATION, + PROP_FILE, + PROP_FILTER, + PROP_LOCAL_ONLY, + PROP_SHOW_HIDDEN, + PROP_MAX_WIDTH_CHARS, + PROP_TITLE, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +G_DEFINE_TYPE_EXTENDED (DzlFileChooserEntry, + dzl_file_chooser_entry, + GTK_TYPE_BIN, + 0, + G_ADD_PRIVATE (DzlFileChooserEntry)) + +static void +dzl_file_chooser_entry_sync_to_dialog (DzlFileChooserEntry *self) +{ + DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self); + GtkWidget *toplevel; + GtkWidget *default_widget; + + g_assert (DZL_IS_FILE_CHOOSER_ENTRY (self)); + + if (priv->dialog == NULL) + return; + + g_object_set (priv->dialog, + "action", priv->action, + "create-folders", priv->create_folders, + "do-overwrite-confirmation", priv->do_overwrite_confirmation, + "local-only", priv->local_only, + "show-hidden", priv->show_hidden, + "filter", priv->filter, + "title", priv->title, + NULL); + + if (priv->file != NULL) + gtk_file_chooser_set_file (GTK_FILE_CHOOSER (priv->dialog), priv->file, NULL); + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self)); + + if (GTK_IS_WINDOW (toplevel)) + gtk_window_set_transient_for (GTK_WINDOW (priv->dialog), GTK_WINDOW (toplevel)); + + default_widget = gtk_dialog_get_widget_for_response (GTK_DIALOG (priv->dialog), + GTK_RESPONSE_OK); + + switch (priv->action) + { + case GTK_FILE_CHOOSER_ACTION_OPEN: + gtk_button_set_label (GTK_BUTTON (default_widget), _("Open")); + break; + + case GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER: + gtk_button_set_label (GTK_BUTTON (default_widget), _("Select")); + break; + + case GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER: + gtk_button_set_label (GTK_BUTTON (default_widget), _("Create")); + break; + + case GTK_FILE_CHOOSER_ACTION_SAVE: + gtk_button_set_label (GTK_BUTTON (default_widget), _("Save")); + break; + + default: + break; + } +} + +static void +dzl_file_chooser_entry_dialog_response (DzlFileChooserEntry *self, + gint response_id, + GtkFileChooserDialog *dialog) +{ + g_assert (DZL_IS_FILE_CHOOSER_ENTRY (self)); + g_assert (GTK_IS_FILE_CHOOSER_DIALOG (dialog)); + + if (response_id == GTK_RESPONSE_OK) + { + g_autoptr(GFile) file = NULL; + + file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); + if (file != NULL) + dzl_file_chooser_entry_set_file (self, file); + } + + gtk_widget_destroy (GTK_WIDGET (dialog)); +} + +static void +dzl_file_chooser_entry_ensure_dialog (DzlFileChooserEntry *self) +{ + DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self); + + g_assert (DZL_IS_FILE_CHOOSER_ENTRY (self)); + + if (priv->dialog == NULL) + { + priv->dialog = g_object_new (GTK_TYPE_FILE_CHOOSER_DIALOG, + "local-only", TRUE, + "modal", TRUE, + NULL); + g_signal_connect_object (priv->dialog, + "response", + G_CALLBACK (dzl_file_chooser_entry_dialog_response), + self, + G_CONNECT_SWAPPED); + g_signal_connect (priv->dialog, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &priv->dialog); + gtk_dialog_add_buttons (GTK_DIALOG (priv->dialog), + _("Cancel"), GTK_RESPONSE_CANCEL, + _("Open"), GTK_RESPONSE_OK, + NULL); + gtk_dialog_set_default_response (GTK_DIALOG (priv->dialog), GTK_RESPONSE_OK); + } + + dzl_file_chooser_entry_sync_to_dialog (self); +} + +static void +dzl_file_chooser_entry_button_clicked (DzlFileChooserEntry *self, + GtkButton *button) +{ + DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self); + + g_assert (DZL_IS_FILE_CHOOSER_ENTRY (self)); + g_assert (GTK_IS_BUTTON (button)); + + dzl_file_chooser_entry_ensure_dialog (self); + gtk_window_present (GTK_WINDOW (priv->dialog)); +} + +static GFile * +file_expand (const gchar *path) +{ + g_autofree gchar *relative = NULL; + g_autofree gchar *scheme = NULL; + + if (path == NULL) + return g_file_new_for_path (g_get_home_dir ()); + + scheme = g_uri_parse_scheme (path); + if (scheme != NULL) + return g_file_new_for_uri (path); + + if (g_path_is_absolute (path)) + return g_file_new_for_path (path); + + relative = g_build_filename (g_get_home_dir (), + path[0] == '~' ? &path[1] : path, + NULL); + + return g_file_new_for_path (relative); +} + +static void +dzl_file_chooser_entry_changed (DzlFileChooserEntry *self, + GtkEntry *entry) +{ + DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self); + g_autoptr(GFile) file = NULL; + + g_assert (DZL_IS_FILE_CHOOSER_ENTRY (self)); + g_assert (GTK_IS_ENTRY (entry)); + + file = file_expand (gtk_entry_get_text (entry)); + + if (g_set_object (&priv->file, file)) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FILE]); +} + +static void +dzl_file_chooser_entry_destroy (GtkWidget *widget) +{ + DzlFileChooserEntry *self = (DzlFileChooserEntry *)widget; + DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self); + + if (priv->dialog != NULL) + gtk_widget_destroy (GTK_WIDGET (priv->dialog)); + + GTK_WIDGET_CLASS (dzl_file_chooser_entry_parent_class)->destroy (widget); +} + +static void +dzl_file_chooser_entry_finalize (GObject *object) +{ + DzlFileChooserEntry *self = (DzlFileChooserEntry *)object; + DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self); + + g_clear_object (&priv->file); + g_clear_object (&priv->filter); + g_clear_pointer (&priv->title, g_free); + + G_OBJECT_CLASS (dzl_file_chooser_entry_parent_class)->finalize (object); +} + +static void +dzl_file_chooser_entry_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlFileChooserEntry *self = DZL_FILE_CHOOSER_ENTRY (object); + DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self); + + switch (prop_id) + { + case PROP_ACTION: + g_value_set_enum (value, priv->action); + break; + + case PROP_LOCAL_ONLY: + g_value_set_boolean (value, priv->local_only); + break; + + case PROP_CREATE_FOLDERS: + g_value_set_boolean (value, priv->create_folders); + break; + + case PROP_DO_OVERWRITE_CONFIRMATION: + g_value_set_boolean (value, priv->do_overwrite_confirmation); + break; + + case PROP_SHOW_HIDDEN: + g_value_set_boolean (value, priv->show_hidden); + break; + + case PROP_FILTER: + g_value_set_object (value, priv->filter); + break; + + case PROP_FILE: + g_value_take_object (value, dzl_file_chooser_entry_get_file (self)); + break; + + case PROP_MAX_WIDTH_CHARS: + g_value_set_int (value, gtk_entry_get_max_width_chars (priv->entry)); + break; + + case PROP_TITLE: + g_value_set_string (value, priv->title); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_file_chooser_entry_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlFileChooserEntry *self = DZL_FILE_CHOOSER_ENTRY (object); + DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self); + + switch (prop_id) + { + case PROP_ACTION: + priv->action = g_value_get_enum (value); + break; + + case PROP_LOCAL_ONLY: + priv->local_only = g_value_get_boolean (value); + break; + + case PROP_CREATE_FOLDERS: + priv->create_folders= g_value_get_boolean (value); + break; + + case PROP_DO_OVERWRITE_CONFIRMATION: + priv->do_overwrite_confirmation = g_value_get_boolean (value); + break; + + case PROP_SHOW_HIDDEN: + priv->show_hidden = g_value_get_boolean (value); + break; + + case PROP_FILTER: + g_clear_object (&priv->filter); + priv->filter = g_value_dup_object (value); + break; + + case PROP_FILE: + dzl_file_chooser_entry_set_file (self, g_value_get_object (value)); + break; + + case PROP_MAX_WIDTH_CHARS: + gtk_entry_set_max_width_chars (priv->entry, g_value_get_int (value)); + break; + + case PROP_TITLE: + g_free (priv->title); + priv->title = g_value_dup_string (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } + + dzl_file_chooser_entry_sync_to_dialog (self); +} + +static void +dzl_file_chooser_entry_class_init (DzlFileChooserEntryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_file_chooser_entry_finalize; + object_class->get_property = dzl_file_chooser_entry_get_property; + object_class->set_property = dzl_file_chooser_entry_set_property; + + widget_class->destroy = dzl_file_chooser_entry_destroy; + + properties [PROP_ACTION] = + g_param_spec_enum ("action", + NULL, + NULL, + GTK_TYPE_FILE_CHOOSER_ACTION, + GTK_FILE_CHOOSER_ACTION_OPEN, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_CREATE_FOLDERS] = + g_param_spec_boolean ("create-folders", + NULL, + NULL, + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_DO_OVERWRITE_CONFIRMATION] = + g_param_spec_boolean ("do-overwrite-confirmation", + NULL, + NULL, + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_LOCAL_ONLY] = + g_param_spec_boolean ("local-only", + NULL, + NULL, + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_HIDDEN] = + g_param_spec_boolean ("show-hidden", + NULL, + NULL, + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_FILTER] = + g_param_spec_object ("filter", + NULL, + NULL, + GTK_TYPE_FILE_FILTER, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_FILE] = + g_param_spec_object ("file", + NULL, + NULL, + G_TYPE_FILE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_MAX_WIDTH_CHARS] = + g_param_spec_int ("max-width-chars", + NULL, + NULL, + -1, + G_MAXINT, + -1, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + NULL, + NULL, + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_file_chooser_entry_init (DzlFileChooserEntry *self) +{ + DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self); + GtkWidget *hbox; + + hbox = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "visible", TRUE, + NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (hbox), "linked"); + gtk_container_add (GTK_CONTAINER (self), hbox); + + priv->entry = g_object_new (GTK_TYPE_ENTRY, + "visible", TRUE, + NULL); + g_signal_connect (priv->entry, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &priv->entry); + g_signal_connect_object (priv->entry, + "changed", + G_CALLBACK (dzl_file_chooser_entry_changed), + self, + G_CONNECT_SWAPPED); + gtk_container_add_with_properties (GTK_CONTAINER (hbox), GTK_WIDGET (priv->entry), + "expand", TRUE, + NULL); + + priv->button = g_object_new (GTK_TYPE_BUTTON, + "label", _("Browse…"), + "visible", TRUE, + NULL); + g_signal_connect_object (priv->button, + "clicked", + G_CALLBACK (dzl_file_chooser_entry_button_clicked), + self, + G_CONNECT_SWAPPED); + g_signal_connect (priv->button, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &priv->button); + gtk_container_add (GTK_CONTAINER (hbox), GTK_WIDGET (priv->button)); +} + +static gchar * +file_collapse (GFile *file) +{ + gchar *path = NULL; + + g_assert (!file || G_IS_FILE (file)); + + if (file == NULL) + return g_strdup (""); + + if (!g_file_is_native (file)) + return g_file_get_uri (file); + + path = g_file_get_path (file); + + if (path == NULL) + return g_strdup (""); + + if (!g_path_is_absolute (path)) + { + g_autofree gchar *freeme = path; + + path = g_build_filename (g_get_home_dir (), freeme, NULL); + } + + if (g_str_has_prefix (path, g_get_home_dir ())) + { + g_autofree gchar *freeme = path; + + path = g_build_filename ("~", + freeme + strlen (g_get_home_dir ()), + NULL); + } + + return path; +} + +/** + * dzl_file_chooser_entry_get_file: + * + * Returns the currently selected file or %NULL if there is no selection. + * + * Returns: (nullable) (transfer full): A #GFile or %NULL. + */ +GFile * +dzl_file_chooser_entry_get_file (DzlFileChooserEntry *self) +{ + DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_FILE_CHOOSER_ENTRY (self), NULL); + + return priv->file ? g_object_ref (priv->file) : NULL; +} + +void +dzl_file_chooser_entry_set_file (DzlFileChooserEntry *self, + GFile *file) +{ + DzlFileChooserEntryPrivate *priv = dzl_file_chooser_entry_get_instance_private (self); + g_autofree gchar *collapsed = NULL; + + g_return_if_fail (DZL_IS_FILE_CHOOSER_ENTRY (self)); + + if (priv->file == file || (priv->file && file && g_file_equal (priv->file, file))) + return; + + if (file != NULL) + g_object_ref (file); + + g_clear_object (&priv->file); + priv->file = file; + + collapsed = file_collapse (file); + gtk_entry_set_text (priv->entry, collapsed); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FILE]); +} diff --git a/src/widgets/dzl-file-chooser-entry.h b/src/widgets/dzl-file-chooser-entry.h new file mode 100644 index 0000000..a430026 --- /dev/null +++ b/src/widgets/dzl-file-chooser-entry.h @@ -0,0 +1,54 @@ +/* dzl-file-chooser-entry.h + * + * Copyright (C) 2016 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_FILE_CHOOSER_ENTRY_H +#define DZL_FILE_CHOOSER_ENTRY_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_FILE_CHOOSER_ENTRY (dzl_file_chooser_entry_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlFileChooserEntry, dzl_file_chooser_entry, DZL, FILE_CHOOSER_ENTRY, GtkBin) + +struct _DzlFileChooserEntryClass +{ + GtkBinClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_file_chooser_entry_new (const gchar *title, + GtkFileChooserAction action); +DZL_AVAILABLE_IN_ALL +GFile *dzl_file_chooser_entry_get_file (DzlFileChooserEntry *self); +DZL_AVAILABLE_IN_ALL +void dzl_file_chooser_entry_set_file (DzlFileChooserEntry *self, + GFile *file); + +G_END_DECLS + +#endif /* DZL_FILE_CHOOSER_ENTRY_H */ diff --git a/src/widgets/dzl-list-box-private.h b/src/widgets/dzl-list-box-private.h new file mode 100644 index 0000000..eb3687b --- /dev/null +++ b/src/widgets/dzl-list-box-private.h @@ -0,0 +1,32 @@ +/* dzl-list-box-private.h + * + * Copyright (C) 2017 Christian Hergert + * + * This program is free software: you *privatean 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 . + */ + +#ifndef DZL_LIST_BOX_PRIVATE_H +#define DZL_LIST_BOX_PRIVATE_H + +#include "dzl-list-box.h" +#include "dzl-list-box-row.h" + +G_BEGIN_DECLS + +gboolean _dzl_list_box_cache (DzlListBox *self, + DzlListBoxRow *row); + +G_END_DECLS + +#endif /* DZL_LIST_BOX_PRIVATE_H */ diff --git a/src/widgets/dzl-list-box-row.c b/src/widgets/dzl-list-box-row.c new file mode 100644 index 0000000..62bd44c --- /dev/null +++ b/src/widgets/dzl-list-box-row.c @@ -0,0 +1,62 @@ +/* dzl-list-box-row.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-list-box-row" + +#include "config.h" + +#include "dzl-list-box.h" +#include "dzl-list-box-private.h" +#include "dzl-list-box-row.h" + +G_DEFINE_ABSTRACT_TYPE (DzlListBoxRow, dzl_list_box_row, GTK_TYPE_LIST_BOX_ROW) + +static void +dzl_list_box_row_dispose (GObject *object) +{ + DzlListBoxRow *self = (DzlListBoxRow *)object; + GtkWidget *parent; + + /* + * Chaining up will cause a lot of things to be deleted such as + * our widget tree starting from this instance. We don't want that + * to happen so that we can reuse the widgets. This should only + * need to be done if the caller has not notified us they will be + * caching the widget first. + */ + + parent = gtk_widget_get_parent (GTK_WIDGET (self)); + if (DZL_IS_LIST_BOX (parent) && + _dzl_list_box_cache (DZL_LIST_BOX (parent), self)) + return; + + G_OBJECT_CLASS (dzl_list_box_row_parent_class)->dispose (object); +} + +static void +dzl_list_box_row_class_init (DzlListBoxRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = dzl_list_box_row_dispose; +} + +static void +dzl_list_box_row_init (DzlListBoxRow *self) +{ +} diff --git a/src/widgets/dzl-list-box-row.h b/src/widgets/dzl-list-box-row.h new file mode 100644 index 0000000..cddee34 --- /dev/null +++ b/src/widgets/dzl-list-box-row.h @@ -0,0 +1,41 @@ +/* dzl-list-box-row.h + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_LIST_BOX_ROW_H +#define DZL_LIST_BOX_ROW_H + +#include + +G_BEGIN_DECLS + +#define DZL_TYPE_LIST_BOX_ROW (dzl_list_box_row_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlListBoxRow, dzl_list_box_row, DZL, LIST_BOX_ROW, GtkListBoxRow) + +struct _DzlListBoxRowClass +{ + GtkListBoxRowClass parent_class; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_list_box_row_new (void); + +G_END_DECLS + +#endif /* DZL_LIST_BOX_ROW_H */ diff --git a/src/widgets/dzl-list-box.c b/src/widgets/dzl-list-box.c new file mode 100644 index 0000000..9bc7812 --- /dev/null +++ b/src/widgets/dzl-list-box.c @@ -0,0 +1,399 @@ +/* dzl-list-box.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-list-box" + +#include "config.h" + +/* + * This widget is just like GtkListBox, except that it allows you to + * very simply re-use existing widgets instead of creating new widgets + * all the time. + * + * It does not, however, try to keep the number of inflated widgets + * low (that would require more work in GtkListBox directly). + * + * This mostly just avoids the overhead of reparsing the template XML + * on every widget (re)creation. + * + * You must subclass DzlListBoxRow for your rows. + */ + +#include "dzl-list-box.h" +#include "dzl-list-box-row.h" + +#define RECYCLE_MAX_DEFAULT 25 + +typedef struct +{ + GListModel *model; + gchar *property_name; + GType row_type; + guint recycle_max; + GQueue trashed_rows; + guint destroying : 1; +} DzlListBoxPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlListBox, dzl_list_box, GTK_TYPE_LIST_BOX) + +enum { + PROP_0, + PROP_PROPERTY_NAME, + PROP_ROW_TYPE, + PROP_ROW_TYPE_NAME, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +gboolean +_dzl_list_box_cache (DzlListBox *self, + DzlListBoxRow *row) +{ + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + + g_assert (DZL_IS_LIST_BOX (self)); + g_assert (DZL_IS_LIST_BOX_ROW (row)); + + if (gtk_widget_get_parent (GTK_WIDGET (row)) != GTK_WIDGET (self)) + { + g_warning ("Attempt to cache row not belonging to list box"); + return FALSE; + } + + if (gtk_widget_in_destruction (GTK_WIDGET (self))) + return FALSE; + + if (priv->trashed_rows.length < priv->recycle_max) + { + g_autoptr(GtkWidget) held = g_object_ref (GTK_WIDGET (row)); + + gtk_list_box_unselect_row (GTK_LIST_BOX (self), GTK_LIST_BOX_ROW (row)); + gtk_container_remove (GTK_CONTAINER (self), GTK_WIDGET (row)); + g_object_set (held, priv->property_name, NULL, NULL); + g_object_force_floating (G_OBJECT (held)); + g_queue_push_head (&priv->trashed_rows, g_steal_pointer (&held)); + + return TRUE; + } + + return FALSE; +} + +static GtkWidget * +dzl_list_box_create_row (gpointer item, + gpointer user_data) +{ + DzlListBox *self = user_data; + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + GtkListBoxRow *row; + + g_assert (G_IS_OBJECT (item)); + g_assert (DZL_IS_LIST_BOX (self)); + + if (priv->trashed_rows.length > 0) + { + row = g_queue_pop_tail (&priv->trashed_rows); + + g_assert (DZL_IS_LIST_BOX_ROW (row)); + g_assert (priv->property_name != NULL); + g_assert (item != NULL); + + g_object_set (row, priv->property_name, item, NULL); + } + else + { + row = g_object_new (priv->row_type, + "visible", TRUE, + priv->property_name, item, + NULL); + } + + g_return_val_if_fail (DZL_IS_LIST_BOX_ROW (row), NULL); + + return GTK_WIDGET (row); +} + +static void +dzl_list_box_constructed (GObject *object) +{ + DzlListBox *self = (DzlListBox *)object; + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + GObjectClass *row_class; + GParamSpec *pspec; + gboolean valid; + + G_OBJECT_CLASS (dzl_list_box_parent_class)->constructed (object); + + if (!g_type_is_a (priv->row_type, GTK_TYPE_LIST_BOX_ROW) || !priv->property_name) + goto failure; + + row_class = g_type_class_ref (priv->row_type); + pspec = g_object_class_find_property (row_class, priv->property_name); + valid = (pspec != NULL) && g_type_is_a (pspec->value_type, G_TYPE_OBJECT); + g_type_class_unref (row_class); + + if (valid) + return; + +failure: + g_warning ("Invalid DzlListBox instantiated, will not work as expected"); + priv->row_type = G_TYPE_INVALID; + g_clear_pointer (&priv->property_name, g_free); +} + +static void +dzl_list_box_destroy (GtkWidget *widget) +{ + DzlListBox *self = (DzlListBox *)widget; + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + GList *rows; + + g_assert (DZL_IS_LIST_BOX (self)); + + priv->destroying = TRUE; + priv->recycle_max = 0; + + rows = priv->trashed_rows.head; + + priv->trashed_rows.head = NULL; + priv->trashed_rows.tail = NULL; + priv->trashed_rows.length = 0; + + g_list_foreach (rows, (GFunc)gtk_widget_destroy, NULL); + g_list_free (rows); + + GTK_WIDGET_CLASS (dzl_list_box_parent_class)->destroy (widget); +} + +static void +dzl_list_box_finalize (GObject *object) +{ + DzlListBox *self = (DzlListBox *)object; + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + + g_clear_pointer (&priv->property_name, g_free); + priv->row_type = G_TYPE_INVALID; + + G_OBJECT_CLASS (dzl_list_box_parent_class)->finalize (object); +} + +static void +dzl_list_box_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlListBox *self = DZL_LIST_BOX (object); + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + + switch (prop_id) + { + case PROP_ROW_TYPE: + g_value_set_gtype (value, priv->row_type); + break; + + case PROP_PROPERTY_NAME: + g_value_set_string (value, priv->property_name); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_list_box_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlListBox *self = DZL_LIST_BOX (object); + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + + switch (prop_id) + { + case PROP_ROW_TYPE: + { + GType gtype = g_value_get_gtype (value); + + if (gtype != G_TYPE_INVALID) + priv->row_type = gtype; + } + break; + + case PROP_ROW_TYPE_NAME: + { + const gchar *name = g_value_get_string (value); + + if (name != NULL) + priv->row_type = g_type_from_name (name); + } + break; + + case PROP_PROPERTY_NAME: + priv->property_name = g_value_dup_string (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_list_box_class_init (DzlListBoxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = dzl_list_box_constructed; + object_class->finalize = dzl_list_box_finalize; + object_class->get_property = dzl_list_box_get_property; + object_class->set_property = dzl_list_box_set_property; + + widget_class->destroy = dzl_list_box_destroy; + + properties [PROP_ROW_TYPE] = + g_param_spec_gtype ("row-type", + "Row Type", + "The GtkListBoxRow or subclass type to instantiate", + GTK_TYPE_LIST_BOX_ROW, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_ROW_TYPE_NAME] = + g_param_spec_string ("row-type-name", + "Row Type Name", + "The name of the GType as a string", + NULL, + (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_PROPERTY_NAME] = + g_param_spec_string ("property-name", + "Property Name", + "The property in which to assign the model item", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_list_box_init (DzlListBox *self) +{ + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + + priv->row_type = G_TYPE_INVALID; + priv->recycle_max = RECYCLE_MAX_DEFAULT; + + g_queue_init (&priv->trashed_rows); +} + +DzlListBox * +dzl_list_box_new (GType row_type, + const gchar *property_name) +{ + g_return_val_if_fail (g_type_is_a (row_type, GTK_TYPE_LIST_BOX_ROW), NULL); + g_return_val_if_fail (property_name != NULL, NULL); + + return g_object_new (DZL_TYPE_LIST_BOX, + "property-name", property_name, + "row-type", row_type, + NULL); +} + +/** + * dzl_list_box_get_model: + * + * Returns: (nullable) (transfer none): A #GListModel or %NULL. + */ +GListModel * +dzl_list_box_get_model (DzlListBox *self) +{ + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_LIST_BOX (self), NULL); + + return priv->model; +} + +GType +dzl_list_box_get_row_type (DzlListBox *self) +{ + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_LIST_BOX (self), G_TYPE_INVALID); + + return priv->row_type; +} + +const gchar * +dzl_list_box_get_property_name (DzlListBox *self) +{ + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_LIST_BOX (self), NULL); + + return priv->property_name; +} + +void +dzl_list_box_set_model (DzlListBox *self, + GListModel *model) +{ + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + + g_return_if_fail (DZL_IS_LIST_BOX (self)); + g_return_if_fail (priv->property_name != NULL); + g_return_if_fail (priv->row_type != G_TYPE_INVALID); + + if (model == NULL) + { + gtk_list_box_bind_model (GTK_LIST_BOX (self), NULL, NULL, NULL, NULL); + return; + } + + gtk_list_box_bind_model (GTK_LIST_BOX (self), + model, + dzl_list_box_create_row, + self, + NULL); +} + +/** + * dzl_list_box_set_recycle_max: + * @self: a #DzlListBox + * @recycle_max: max number of rows to cache + * + * Sets the max number of rows to cache for reuse. Set to 0 to return + * to the default. + * + * Since: 3.28 + */ +void +dzl_list_box_set_recycle_max (DzlListBox *self, + guint recycle_max) +{ + DzlListBoxPrivate *priv = dzl_list_box_get_instance_private (self); + + g_return_if_fail (DZL_IS_LIST_BOX (self)); + + if (recycle_max == 0) + priv->recycle_max = RECYCLE_MAX_DEFAULT; + else + priv->recycle_max = recycle_max; +} diff --git a/src/widgets/dzl-list-box.h b/src/widgets/dzl-list-box.h new file mode 100644 index 0000000..f0c7ced --- /dev/null +++ b/src/widgets/dzl-list-box.h @@ -0,0 +1,61 @@ +/* dzl-list-box.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_LIST_BOX_H +#define DZL_LIST_BOX_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_LIST_BOX (dzl_list_box_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlListBox, dzl_list_box, DZL, LIST_BOX, GtkListBox) + +struct _DzlListBoxClass +{ + GtkListBoxClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; +}; + +DZL_AVAILABLE_IN_ALL +DzlListBox *dzl_list_box_new (GType row_type, + const gchar *property_name); +DZL_AVAILABLE_IN_ALL +GType dzl_list_box_get_row_type (DzlListBox *self); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_list_box_get_property_name (DzlListBox *self); +DZL_AVAILABLE_IN_ALL +GListModel *dzl_list_box_get_model (DzlListBox *self); +DZL_AVAILABLE_IN_ALL +void dzl_list_box_set_model (DzlListBox *self, + GListModel *model); +DZL_AVAILABLE_IN_3_28 +void dzl_list_box_set_recycle_max (DzlListBox *self, + guint recycle_max); + +G_END_DECLS + +#endif /* DZL_LIST_BOX_H */ diff --git a/src/widgets/dzl-multi-paned.c b/src/widgets/dzl-multi-paned.c new file mode 100644 index 0000000..ad1683d --- /dev/null +++ b/src/widgets/dzl-multi-paned.c @@ -0,0 +1,2244 @@ +/* dzl-multi-paned.c + * + * Copyright (C) 2016 Christian Hergert + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define G_LOG_DOMAIN "dzl-multi-paned" + +#include "config.h" + +#include "widgets/dzl-multi-paned.h" +#include "util/dzl-util-private.h" + +#define HANDLE_WIDTH 10 +#define HANDLE_HEIGHT 10 + +#define IS_HORIZONTAL(o) (o == GTK_ORIENTATION_HORIZONTAL) + +/** + * SECTION:dzlmultipaned + * @title: DzlMultiPaned + * @short_description: A widget with multiple adjustable panes + * + * This widget is similar to #GtkPaned except that it allows adding more than + * two children to the widget. For each additional child added to the + * #DzlMultiPaned, an additional resize grip is added. + */ + +typedef struct +{ + /* + * The child widget in question. + */ + GtkWidget *widget; + + /* + * The input only window for resize grip. + * Has a cursor associated with it. + */ + GdkWindow *handle; + + /* + * The position the handle has been dragged to. + * This is used to adjust size requests. + */ + gint position; + + /* + * Cached size requests to avoid extra sizing calls during + * the layout procedure. + */ + GtkRequisition min_req; + GtkRequisition nat_req; + + /* + * A cached size allocation used during the size_allocate() + * cycle. This allows us to do a first pass to allocate + * natural sizes, and then followup when dealing with + * expanding children. + */ + GtkAllocation alloc; + + /* + * If the position field has been set. + */ + guint position_set : 1; +} DzlMultiPanedChild; + +typedef struct +{ + /* + * A GArray of DzlMultiPanedChild containing everything we need to + * do size requests, drag operations, resize handles, and temporary + * space needed in such operations. + */ + GArray *children; + + /* + * The gesture used for dragging resize handles. + * + * TODO: GtkPaned now uses two gestures, one for mouse and one for touch. + * We should do the same as it improved things quite a bit. + */ + GtkGesturePan *gesture; + + /* + * For GtkOrientable:orientation. + */ + GtkOrientation orientation; + + /* + * This is the child that is currently being dragged. Keep in mind that + * the drag handle is immediately after the child. So the final visible + * child has the handle input-only window hidden. + */ + DzlMultiPanedChild *drag_begin; + + /* + * The position (width or height) of the child when the drag began. + * We use the pan delta offset to determine what the size should be + * by adding (or subtracting) to this value. + */ + gint drag_begin_position; + + /* + * If we are dragging a handle in a fashion that would shrink the + * previous widgets, we need to track how much to subtract from their + * target allocations. This is set during the drag operation and used + * in allocation_stage_drag_overflow() to adjust the neighbors. + */ + gint drag_extra_offset; +} DzlMultiPanedPrivate; + +typedef struct +{ + DzlMultiPanedChild **children; + guint n_children; + GtkOrientation orientation; + GtkAllocation top_alloc; + gint avail_width; + gint avail_height; + gint handle_size; +} AllocationState; + +typedef void (*AllocationStage) (DzlMultiPaned *self, + AllocationState *state); + +static void allocation_stage_allocate (DzlMultiPaned *self, + AllocationState *state); +static void allocation_stage_borders (DzlMultiPaned *self, + AllocationState *state); +static void allocation_stage_cache_request (DzlMultiPaned *self, + AllocationState *state); +static void allocation_stage_drag_overflow (DzlMultiPaned *self, + AllocationState *state); +static void allocation_stage_expand (DzlMultiPaned *self, + AllocationState *state); +static void allocation_stage_handles (DzlMultiPaned *self, + AllocationState *state); +static void allocation_stage_minimums (DzlMultiPaned *self, + AllocationState *state); +static void allocation_stage_naturals (DzlMultiPaned *self, + AllocationState *state); +static void allocation_stage_positions (DzlMultiPaned *self, + AllocationState *state); + +G_DEFINE_TYPE_EXTENDED (DzlMultiPaned, dzl_multi_paned, GTK_TYPE_CONTAINER, 0, + G_ADD_PRIVATE (DzlMultiPaned) + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) + +enum { + PROP_0, + PROP_ORIENTATION, + N_PROPS +}; + +enum { + CHILD_PROP_0, + CHILD_PROP_INDEX, + CHILD_PROP_POSITION, + N_CHILD_PROPS +}; + +enum { + STYLE_PROP_0, + STYLE_PROP_HANDLE_SIZE, + N_STYLE_PROPS +}; + +enum { + RESIZE_DRAG_BEGIN, + RESIZE_DRAG_END, + N_SIGNALS +}; + +/* + * TODO: An obvious optimization here would be to move the constant + * branches outside the loops. + */ + +static GParamSpec *properties [N_PROPS]; +static GParamSpec *child_properties [N_CHILD_PROPS]; +static GParamSpec *style_properties [N_STYLE_PROPS]; +static guint signals [N_SIGNALS]; +static AllocationStage allocation_stages[] = { + allocation_stage_borders, + allocation_stage_cache_request, + allocation_stage_minimums, + allocation_stage_handles, + allocation_stage_positions, + allocation_stage_drag_overflow, + allocation_stage_naturals, + allocation_stage_expand, + allocation_stage_allocate, +}; + +static void +dzl_multi_paned_reset_positions (DzlMultiPaned *self) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + child->position = -1; + child->position_set = FALSE; + + gtk_container_child_notify_by_pspec (GTK_CONTAINER (self), + child->widget, + child_properties [CHILD_PROP_POSITION]); + } + + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static DzlMultiPanedChild * +dzl_multi_paned_get_next_visible_child (DzlMultiPaned *self, + DzlMultiPanedChild *child) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (child != NULL); + g_assert (priv->children != NULL); + g_assert (priv->children->len > 0); + + i = child - ((DzlMultiPanedChild *)(gpointer)priv->children->data); + + for (++i; i < priv->children->len; i++) + { + DzlMultiPanedChild *next = &g_array_index (priv->children, DzlMultiPanedChild, i); + + if (gtk_widget_get_visible (next->widget)) + return next; + } + + return NULL; +} + +static gboolean +dzl_multi_paned_is_last_visible_child (DzlMultiPaned *self, + DzlMultiPanedChild *child) +{ + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (child != NULL); + + return !dzl_multi_paned_get_next_visible_child (self, child); +} + +static void +dzl_multi_paned_get_handle_rect (DzlMultiPaned *self, + DzlMultiPanedChild *child, + GdkRectangle *handle_rect) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GtkAllocation alloc; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (child != NULL); + g_assert (handle_rect != NULL); + + handle_rect->x = -1; + handle_rect->y = -1; + handle_rect->width = 0; + handle_rect->height = 0; + + if (!gtk_widget_get_visible (child->widget) || + !gtk_widget_get_realized (child->widget)) + return; + + if (dzl_multi_paned_is_last_visible_child (self, child)) + return; + + gtk_widget_get_allocation (child->widget, &alloc); + + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + { + handle_rect->x = alloc.x + alloc.width - (HANDLE_WIDTH / 2); + handle_rect->width = HANDLE_WIDTH; + handle_rect->y = alloc.y; + handle_rect->height = alloc.height; + } + else + { + handle_rect->x = alloc.x; + handle_rect->width = alloc.width; + handle_rect->y = alloc.y + alloc.height - (HANDLE_HEIGHT / 2); + handle_rect->height = HANDLE_HEIGHT; + } +} + +static void +dzl_multi_paned_create_child_handle (DzlMultiPaned *self, + DzlMultiPanedChild *child) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GdkWindowAttr attributes = { 0 }; + GdkDisplay *display; + GdkWindow *parent; + const char *cursor_name; + GdkRectangle handle_rect; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (child != NULL); + g_assert (child->handle == NULL); + + display = gtk_widget_get_display (GTK_WIDGET (self)); + parent = gtk_widget_get_window (GTK_WIDGET (self)); + + cursor_name = (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + ? "col-resize" + : "row-resize"; + + dzl_multi_paned_get_handle_rect (self, child, &handle_rect); + + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_ONLY; + attributes.x = handle_rect.x; + attributes.y = -handle_rect.y; + attributes.width = handle_rect.width; + attributes.height = handle_rect.height; + attributes.visual = gtk_widget_get_visual (GTK_WIDGET (self)); + attributes.event_mask = (GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_ENTER_NOTIFY_MASK | + GDK_LEAVE_NOTIFY_MASK | + GDK_POINTER_MOTION_MASK); + attributes.cursor = gdk_cursor_new_from_name (display, cursor_name); + + child->handle = gdk_window_new (parent, &attributes, GDK_WA_CURSOR); + gtk_widget_register_window (GTK_WIDGET (self), child->handle); + + g_clear_object (&attributes.cursor); +} + +static gint +dzl_multi_paned_calc_handle_size (DzlMultiPaned *self) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + gint visible_children = 0; + gint handle_size = 1; + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + + gtk_widget_style_get (GTK_WIDGET (self), "handle-size", &handle_size, NULL); + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + if (gtk_widget_get_visible (child->widget)) + visible_children++; + } + + return MAX (0, (visible_children - 1) * handle_size); +} + +static void +dzl_multi_paned_destroy_child_handle (DzlMultiPaned *self, + DzlMultiPanedChild *child) +{ + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (child != NULL); + + if (child->handle != NULL) + { + gtk_widget_unregister_window (GTK_WIDGET (self), child->handle); + gdk_window_destroy (child->handle); + child->handle = NULL; + } +} + +static void +dzl_multi_paned_update_child_handles (DzlMultiPaned *self) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GtkWidget *widget = GTK_WIDGET (self); + + if (gtk_widget_get_realized (widget)) + { + GdkCursor *cursor; + guint i; + + if (gtk_widget_is_sensitive (widget)) + cursor = gdk_cursor_new_from_name (gtk_widget_get_display (widget), + priv->orientation == GTK_ORIENTATION_HORIZONTAL + ? "col-resize" + : "row-resize"); + else + cursor = NULL; + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + gdk_window_set_cursor (child->handle, cursor); + } + + if (cursor) + g_object_unref (cursor); + } +} + +static DzlMultiPanedChild * +dzl_multi_paned_get_child (DzlMultiPaned *self, + GtkWidget *widget) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (GTK_IS_WIDGET (widget)); + + for (guint i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + if (child->widget == widget) + return child; + } + + g_assert_not_reached (); + + return NULL; +} + +static gint +dzl_multi_paned_get_child_index (DzlMultiPaned *self, + GtkWidget *widget) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (GTK_IS_WIDGET (widget)); + + for (guint i = 0; i < priv->children->len; i++) + { + const DzlMultiPanedChild *ele = &g_array_index (priv->children, DzlMultiPanedChild, i); + + if (ele->widget == widget) + return i; + } + + return -1; +} + +static void +dzl_multi_paned_set_child_index (DzlMultiPaned *self, + GtkWidget *widget, + gint index) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (GTK_IS_WIDGET (widget)); + g_assert (index >= -1); + g_assert (priv->children->len > 0); + + if (index < 0) + index = priv->children->len - 1; + + index = CLAMP (index, 0, priv->children->len - 1); + + for (guint i = 0; i < priv->children->len; i++) + { + const DzlMultiPanedChild *ele = &g_array_index (priv->children, DzlMultiPanedChild, i); + + if (ele->widget == widget) + { + DzlMultiPanedChild copy = { 0 }; + + copy.widget = ele->widget; + copy.handle = ele->handle; + copy.position = -1; + copy.position_set = FALSE; + + g_array_remove_index (priv->children, i); + g_array_insert_val (priv->children, index, copy); + gtk_container_child_notify_by_pspec (GTK_CONTAINER (self), widget, + child_properties [CHILD_PROP_INDEX]); + gtk_widget_queue_resize (GTK_WIDGET (self)); + + break; + } + } +} + +static gint +dzl_multi_paned_get_child_position (DzlMultiPaned *self, + GtkWidget *widget) +{ + DzlMultiPanedChild *child; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (GTK_IS_WIDGET (widget)); + + child = dzl_multi_paned_get_child (self, widget); + + return child->position; +} + +static void +dzl_multi_paned_set_child_position (DzlMultiPaned *self, + GtkWidget *widget, + gint position) +{ + DzlMultiPanedChild *child; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (GTK_IS_WIDGET (widget)); + g_assert (position >= -1); + + child = dzl_multi_paned_get_child (self, widget); + + if (child->position != position) + { + child->position = position; + child->position_set = (position != -1); + gtk_container_child_notify_by_pspec (GTK_CONTAINER (self), widget, + child_properties [CHILD_PROP_POSITION]); + gtk_widget_queue_resize (GTK_WIDGET (self)); + } +} + +static void +dzl_multi_paned_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlMultiPaned *self = (DzlMultiPaned *)container; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + DzlMultiPanedChild child = { 0 }; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (GTK_IS_WIDGET (widget)); + + child.widget = g_object_ref_sink (widget); + child.position = -1; + + if (gtk_widget_get_realized (GTK_WIDGET (self))) + dzl_multi_paned_create_child_handle (self, &child); + + gtk_widget_set_parent (widget, GTK_WIDGET (self)); + + g_array_append_val (priv->children, child); + + dzl_multi_paned_reset_positions (self); + + gtk_gesture_set_state (GTK_GESTURE (priv->gesture), GTK_EVENT_SEQUENCE_DENIED); + + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static void +dzl_multi_paned_remove (GtkContainer *container, + GtkWidget *widget) +{ + DzlMultiPaned *self = (DzlMultiPaned *)container; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (GTK_IS_WIDGET (widget)); + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + if (child->widget == widget) + { + dzl_multi_paned_destroy_child_handle (self, child); + + g_array_remove_index (priv->children, i); + child = NULL; + + gtk_widget_unparent (widget); + g_object_unref (widget); + + break; + } + } + + dzl_multi_paned_reset_positions (self); + + gtk_gesture_set_state (GTK_GESTURE (priv->gesture), GTK_EVENT_SEQUENCE_DENIED); + + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static void +dzl_multi_paned_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer user_data) +{ + DzlMultiPaned *self = (DzlMultiPaned *)container; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + gint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (callback != NULL); + + for (i = priv->children->len; i > 0; i--) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i - 1); + + callback (child->widget, user_data); + } +} + +static GtkSizeRequestMode +dzl_multi_paned_get_request_mode (GtkWidget *widget) +{ + DzlMultiPaned *self = (DzlMultiPaned *)widget; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + + g_assert (DZL_IS_MULTI_PANED (self)); + + return (priv->orientation == GTK_ORIENTATION_HORIZONTAL) ? GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT + : GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + +static void +dzl_multi_paned_get_preferred_height (GtkWidget *widget, + gint *min_height, + gint *nat_height) +{ + DzlMultiPaned *self = (DzlMultiPaned *)widget; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GtkStyleContext *style_context; + GtkBorder borders; + guint i; + gint real_min_height = 0; + gint real_nat_height = 0; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (min_height != NULL); + g_assert (nat_height != NULL); + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + gint child_min_height = 0; + gint child_nat_height = 0; + + if (gtk_widget_get_visible (child->widget)) + { + gtk_widget_get_preferred_height (child->widget, &child_min_height, &child_nat_height); + + if (priv->orientation == GTK_ORIENTATION_VERTICAL) + { + real_min_height += child_min_height; + real_nat_height += child_nat_height; + } + else + { + real_min_height = MAX (real_min_height, child_min_height); + real_nat_height = MAX (real_nat_height, child_nat_height); + } + } + } + + if (priv->orientation == GTK_ORIENTATION_VERTICAL) + { + gint handle_size = dzl_multi_paned_calc_handle_size (self); + + real_min_height += handle_size; + real_nat_height += handle_size; + } + + *min_height = real_min_height; + *nat_height = real_nat_height; + + style_context = gtk_widget_get_style_context (widget); + dzl_gtk_style_context_get_borders (style_context, &borders); + + *min_height += borders.top + borders.bottom; + *nat_height += borders.top + borders.bottom; +} + +static void +dzl_multi_paned_get_child_preferred_height_for_width (DzlMultiPaned *self, + DzlMultiPanedChild *children, + gint n_children, + gint width, + gint *min_height, + gint *nat_height) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + DzlMultiPanedChild *child = children; + gint child_min_height = 0; + gint child_nat_height = 0; + gint neighbor_min_height = 0; + gint neighbor_nat_height = 0; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (n_children == 0 || children != NULL); + g_assert (min_height != NULL); + g_assert (nat_height != NULL); + + *min_height = 0; + *nat_height = 0; + + if (n_children == 0) + return; + + if (gtk_widget_get_visible (child->widget)) + gtk_widget_get_preferred_height_for_width (child->widget, + width, + &child_min_height, + &child_nat_height); + + dzl_multi_paned_get_child_preferred_height_for_width (self, + children + 1, + n_children - 1, + width, + &neighbor_min_height, + &neighbor_nat_height); + + if (priv->orientation == GTK_ORIENTATION_VERTICAL) + { + *min_height = child_min_height + neighbor_min_height; + *nat_height = child_nat_height + neighbor_nat_height; + } + else + { + *min_height = MAX (child_min_height, neighbor_min_height); + *nat_height = MAX (child_nat_height, neighbor_nat_height); + } +} + +static void +dzl_multi_paned_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *min_height, + gint *nat_height) +{ + DzlMultiPaned *self = (DzlMultiPaned *)widget; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GtkStyleContext *style_context; + GtkBorder borders; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (min_height != NULL); + g_assert (nat_height != NULL); + + *min_height = 0; + *nat_height = 0; + + dzl_multi_paned_get_child_preferred_height_for_width (self, + (DzlMultiPanedChild *)(gpointer)priv->children->data, + priv->children->len, + width, + min_height, + nat_height); + + if (priv->orientation == GTK_ORIENTATION_VERTICAL) + { + gint handle_size = dzl_multi_paned_calc_handle_size (self); + + *min_height += handle_size; + *nat_height += handle_size; + } + + style_context = gtk_widget_get_style_context (widget); + dzl_gtk_style_context_get_borders (style_context, &borders); + + *min_height += borders.top + borders.bottom; + *nat_height += borders.top + borders.bottom; +} + +static void +dzl_multi_paned_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + DzlMultiPaned *self = (DzlMultiPaned *)widget; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GtkStyleContext *style_context; + GtkBorder borders; + guint i; + gint real_min_width = 0; + gint real_nat_width = 0; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (min_width != NULL); + g_assert (nat_width != NULL); + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + gint child_min_width = 0; + gint child_nat_width = 0; + + if (gtk_widget_get_visible (child->widget)) + { + gtk_widget_get_preferred_width (child->widget, &child_min_width, &child_nat_width); + + if (priv->orientation == GTK_ORIENTATION_VERTICAL) + { + real_min_width = MAX (real_min_width, child_min_width); + real_nat_width = MAX (real_nat_width, child_nat_width); + } + else + { + real_min_width += child_min_width; + real_nat_width += child_nat_width; + } + } + } + + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + { + gint handle_size = dzl_multi_paned_calc_handle_size (self); + + real_min_width += handle_size; + real_nat_width += handle_size; + } + + *min_width = real_min_width; + *nat_width = real_nat_width; + + style_context = gtk_widget_get_style_context (widget); + dzl_gtk_style_context_get_borders (style_context, &borders); + + *min_width += borders.left + borders.right; + *nat_width += borders.left + borders.right; +} + +static void +dzl_multi_paned_get_child_preferred_width_for_height (DzlMultiPaned *self, + DzlMultiPanedChild *children, + gint n_children, + gint height, + gint *min_width, + gint *nat_width) +{ + DzlMultiPanedChild *child = children; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + gint child_min_width = 0; + gint child_nat_width = 0; + gint neighbor_min_width = 0; + gint neighbor_nat_width = 0; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (n_children == 0 || children != NULL); + g_assert (min_width != NULL); + g_assert (nat_width != NULL); + + *min_width = 0; + *nat_width = 0; + + if (n_children == 0) + return; + + if (gtk_widget_get_visible (child->widget)) + gtk_widget_get_preferred_width_for_height (child->widget, + height, + &child_min_width, + &child_nat_width); + + dzl_multi_paned_get_child_preferred_width_for_height (self, + children + 1, + n_children - 1, + height, + &neighbor_min_width, + &neighbor_nat_width); + + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + { + *min_width = child_min_width + neighbor_min_width; + *nat_width = child_nat_width + neighbor_nat_width; + } + else + { + *min_width = MAX (child_min_width, neighbor_min_width); + *nat_width = MAX (child_nat_width, neighbor_nat_width); + } +} + +static void +dzl_multi_paned_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *min_width, + gint *nat_width) +{ + DzlMultiPaned *self = (DzlMultiPaned *)widget; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GtkStyleContext *style_context; + GtkBorder borders; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (min_width != NULL); + g_assert (nat_width != NULL); + + dzl_multi_paned_get_child_preferred_width_for_height (self, + (DzlMultiPanedChild *)(gpointer)priv->children->data, + priv->children->len, + height, + min_width, + nat_width); + + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + { + gint handle_size = dzl_multi_paned_calc_handle_size (self); + + *min_width += handle_size; + *nat_width += handle_size; + } + + style_context = gtk_widget_get_style_context (widget); + dzl_gtk_style_context_get_borders (style_context, &borders); + + *min_width += borders.left + borders.right; + *nat_width += borders.left + borders.right; +} + +static void +allocation_stage_handles (DzlMultiPaned *self, + AllocationState *state) +{ + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (state != NULL); + g_assert (state->children != NULL); + g_assert (state->n_children > 0); + + /* + * Push each child allocation forward by the sum handle widths up to + * their position in the paned. + */ + + for (i = 1; i < state->n_children; i++) + { + DzlMultiPanedChild *child = state->children [i]; + + if (IS_HORIZONTAL (state->orientation)) + child->alloc.x += (i * state->handle_size); + else + child->alloc.y += (i * state->handle_size); + } + + if (IS_HORIZONTAL (state->orientation)) + state->avail_width -= (state->n_children - 1) * state->handle_size; + else + state->avail_height -= (state->n_children - 1) * state->handle_size; +} + +static void +allocation_stage_minimums (DzlMultiPaned *self, + AllocationState *state) +{ + gint next_x; + gint next_y; + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (state != NULL); + g_assert (state->children != NULL); + g_assert (state->n_children > 0); + + next_x = state->top_alloc.x; + next_y = state->top_alloc.y; + + for (i = 0; i < state->n_children; i++) + { + DzlMultiPanedChild *child = state->children [i]; + + if (IS_HORIZONTAL (state->orientation)) + { + child->alloc.x = next_x; + child->alloc.y = state->top_alloc.y; + child->alloc.width = child->min_req.width; + child->alloc.height = state->top_alloc.height; + + next_x = child->alloc.x + child->alloc.width; + + state->avail_width -= child->alloc.width; + } + else + { + child->alloc.x = state->top_alloc.x; + child->alloc.y = next_y; + child->alloc.width = state->top_alloc.width; + child->alloc.height = child->min_req.height; + + next_y = child->alloc.y + child->alloc.height; + + state->avail_height -= child->alloc.height; + } + } +} + +static void +allocation_stage_naturals (DzlMultiPaned *self, + AllocationState *state) +{ + gint x_adjust = 0; + gint y_adjust = 0; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (state != NULL); + g_assert (state->children != NULL); + g_assert (state->n_children > 0); + + for (guint i = 0; i < state->n_children; i++) + { + DzlMultiPanedChild *child = state->children [i]; + + child->alloc.x += x_adjust; + child->alloc.y += y_adjust; + + if (!child->position_set) + { + if (IS_HORIZONTAL (state->orientation)) + { + if (child->nat_req.width > child->alloc.width) + { + gint adjust = MIN (state->avail_width, child->nat_req.width - child->alloc.width); + + child->alloc.width += adjust; + state->avail_width -= adjust; + x_adjust += adjust; + } + } + else + { + if (child->nat_req.height > child->alloc.height) + { + gint adjust = MIN (state->avail_height, child->nat_req.height - child->alloc.height); + + child->alloc.height += adjust; + state->avail_height -= adjust; + y_adjust += adjust; + } + } + } + } +} + +static void +allocation_stage_borders (DzlMultiPaned *self, + AllocationState *state) +{ + GtkStyleContext *style_context; + GtkBorder borders; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (state != NULL); + g_assert (state->children != NULL); + g_assert (state->n_children > 0); + + /* + * This subtracts the border+padding from the allocation area so the + * children are guaranteed to fall within that area. + */ + + style_context = gtk_widget_get_style_context (GTK_WIDGET (self)); + dzl_gtk_style_context_get_borders (style_context, &borders); + + state->top_alloc.x += borders.left; + state->top_alloc.y += borders.right; + state->top_alloc.width -= (borders.left + borders.right); + state->top_alloc.height -= (borders.top + borders.bottom); + + if (state->top_alloc.width < 0) + state->top_alloc.width = 0; + + if (state->top_alloc.height < 0) + state->top_alloc.height = 0; + + state->avail_width = state->top_alloc.width; + state->avail_height = state->top_alloc.height; +} + +static void +allocation_stage_cache_request (DzlMultiPaned *self, + AllocationState *state) +{ + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (state != NULL); + g_assert (state->children != NULL); + g_assert (state->n_children > 0); + + for (i = 0; i < state->n_children; i++) + { + DzlMultiPanedChild *child = state->children [i]; + + if (IS_HORIZONTAL (state->orientation)) + gtk_widget_get_preferred_width_for_height (child->widget, + state->avail_height, + &child->min_req.width, + &child->nat_req.width); + else + gtk_widget_get_preferred_height_for_width (child->widget, + state->avail_width, + &child->min_req.height, + &child->nat_req.height); + } +} + +static void +allocation_stage_positions (DzlMultiPaned *self, + AllocationState *state) +{ + gint x_adjust = 0; + gint y_adjust = 0; + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (state != NULL); + g_assert (state->children != NULL); + g_assert (state->n_children > 0); + + /* + * Child may have a position set, which happens when dragging the input + * window (handle) to resize the child. If so, we want to try to allocate + * extra space above the minimum size. + */ + + for (i = 0; i < state->n_children; i++) + { + DzlMultiPanedChild *child = state->children [i]; + + child->alloc.x += x_adjust; + child->alloc.y += y_adjust; + + if (child->position_set) + { + if (IS_HORIZONTAL (state->orientation)) + { + if (child->position > child->alloc.width) + { + gint adjust = MIN (state->avail_width, child->position - child->alloc.width); + + child->alloc.width += adjust; + state->avail_width -= adjust; + x_adjust += adjust; + } + } + else + { + if (child->position > child->alloc.height) + { + gint adjust = MIN (state->avail_height, child->position - child->alloc.height); + + child->alloc.height += adjust; + state->avail_height -= adjust; + y_adjust += adjust; + } + } + } + } +} + +static void +allocation_stage_drag_overflow (DzlMultiPaned *self, + AllocationState *state) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + guint drag_index; + gint j; + gint drag_overflow; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (state != NULL); + g_assert (state->children != NULL); + g_assert (state->n_children > 0); + + if (priv->drag_begin == NULL) + return; + + drag_overflow = ABS (priv->drag_extra_offset); + + for (drag_index = 0; drag_index < state->n_children; drag_index++) + if (state->children [drag_index] == priv->drag_begin) + break; + + if (drag_index == 0 || + drag_index >= state->n_children || + state->children [drag_index] != priv->drag_begin) + return; + + /* + * If the user is dragging and we have run out of room in the drag + * child, then we need to start stealing space from the previous + * items. + * + * This works our way back to the beginning from the drag child + * stealing available space and giving it to the child *AFTER* the + * drag item. This is because the drag handle is after the drag + * child, so logically to the user, its drag_index+1. + */ + + for (j = (int)drag_index; j >= 0 && drag_overflow > 0; j--) + { + DzlMultiPanedChild *child = state->children [j]; + guint k; + gint adjust = 0; + + if (IS_HORIZONTAL (state->orientation)) + { + if (child->alloc.width > child->min_req.width) + { + if (drag_overflow > (child->alloc.width - child->min_req.width)) + adjust = child->alloc.width - child->min_req.width; + else + adjust = drag_overflow; + drag_overflow -= adjust; + child->alloc.width -= adjust; + state->children [drag_index + 1]->alloc.width += adjust; + } + } + else + { + if (child->alloc.height > child->min_req.height) + { + if (drag_overflow > (child->alloc.height - child->min_req.height)) + adjust = child->alloc.height - child->min_req.height; + else + adjust = drag_overflow; + drag_overflow -= adjust; + child->alloc.height -= adjust; + state->children [drag_index + 1]->alloc.height += adjust; + } + } + + /* + * Now walk back forward and adjust x/y offsets for all of the + * children that will have just shifted. + */ + + for (k = j + 1; k <= drag_index + 1; k++) + { + DzlMultiPanedChild *neighbor = state->children [k]; + + if (IS_HORIZONTAL (state->orientation)) + neighbor->alloc.x -= adjust; + else + neighbor->alloc.y -= adjust; + } + } +} + +static gint +sort_children_horizontal (const DzlMultiPanedChild * const *first, + const DzlMultiPanedChild * const *second) +{ + return (*first)->alloc.width - (*second)->alloc.width; +} + +static gint +sort_children_vertical (const DzlMultiPanedChild * const *first, + const DzlMultiPanedChild * const *second) +{ + return (*first)->alloc.height - (*second)->alloc.height; +} + +static void +allocation_stage_expand (DzlMultiPaned *self, + AllocationState *state) +{ + g_autoptr(GPtrArray) expanding = NULL; + gint x_adjust = 0; + gint y_adjust = 0; + gint adjust; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (state != NULL); + g_assert (state->children != NULL); + g_assert (state->n_children > 0); + + if (state->n_children == 1) + { + DzlMultiPanedChild *child = state->children [0]; + + /* + * Special case for single child, just expand to the + * available space. Ideally we would have much shorter + * allocation stages in this case. + */ + + if (gtk_widget_compute_expand (child->widget, state->orientation)) + { + if (IS_HORIZONTAL (state->orientation)) + child->alloc.width = state->top_alloc.width; + else + child->alloc.height = state->top_alloc.height; + } + + return; + } + + /* + * First, we need to collect all of the children who are expanding + * in the direction matching our orientation. After that, we will + * sort them by their allocation size so that we can divy out extra + * allocation starting from the smallest. This will give us an effect + * of homogeneous size when all children are set to expand and enough + * space is available. + */ + + expanding = g_ptr_array_new (); + + for (guint i = 0; i < state->n_children; i++) + { + DzlMultiPanedChild *child = state->children [i]; + + if (!child->position_set) + { + if (gtk_widget_compute_expand (child->widget, state->orientation)) + g_ptr_array_add (expanding, child); + } + } + + if (expanding->len == 0) + goto fill_last; + + /* Now sort, smallest first, based on size in given orientation. */ + if (IS_HORIZONTAL (state->orientation)) + g_ptr_array_sort (expanding, (GCompareFunc) sort_children_horizontal); + else + g_ptr_array_sort (expanding, (GCompareFunc) sort_children_vertical); + + /* + * While we have additional space available to hand out, allocate + * as much space as it takes to get to the next item in the array. + * If we run out of additional space, that is okay (as long as we + * dont hand out more than we have). + */ + + g_assert (expanding->len > 0); + + for (guint i = 0; i < expanding->len - 1; i++) + { + DzlMultiPanedChild *child = g_ptr_array_index (expanding, i); + DzlMultiPanedChild *next = g_ptr_array_index (expanding, i + 1); + + if (IS_HORIZONTAL (state->orientation)) + { + guint j; + + g_assert (next->alloc.width >= child->alloc.width); + + adjust = next->alloc.width - child->alloc.width; + if (adjust > state->avail_width) + adjust = state->avail_width; + + child->alloc.width += adjust; + state->avail_width -= adjust; + + /* Adjust X of children following */ + for (j = 0; j < state->n_children; j++) + if (state->children[j] == child) + break; + for (++j; j < state->n_children; j++) + state->children[j]->alloc.x += adjust; + + g_assert (state->avail_width >= 0); + + if (state->avail_width == 0) + break; + } + else + { + guint j; + + g_assert (next->alloc.height >= child->alloc.height); + + adjust = next->alloc.height - child->alloc.height; + if (adjust > state->avail_height) + adjust = state->avail_height; + + child->alloc.height += adjust; + state->avail_height -= adjust; + + /* Adjust Y of children following */ + for (j = 0; j < state->n_children; j++) + if (state->children[j] == child) + break; + for (++j; j < state->n_children; j++) + state->children[j]->alloc.y += adjust; + + g_assert (state->avail_height >= 0); + + if (state->avail_height == 0) + break; + } + } + + /* + * If we still have more space we can divy out, then do the normal + * expansion case now that we are starting with evenly sized + * children. + */ + + if (IS_HORIZONTAL (state->orientation)) + adjust = state->avail_width / expanding->len; + else + adjust = state->avail_height / expanding->len; + + for (guint i = 0; i < state->n_children; i++) + { + DzlMultiPanedChild *child = state->children [i]; + + child->alloc.x += x_adjust; + child->alloc.y += y_adjust; + + if (!child->position_set) + { + if (gtk_widget_compute_expand (child->widget, state->orientation)) + { + if (IS_HORIZONTAL (state->orientation)) + { + child->alloc.width += adjust; + state->avail_width -= adjust; + x_adjust += adjust; + } + else + { + child->alloc.height += adjust; + state->avail_height -= adjust; + y_adjust += adjust; + } + } + } + } + +fill_last: + + if (IS_HORIZONTAL (state->orientation)) + { + if (state->avail_width > 0) + { + state->children [state->n_children - 1]->alloc.width += state->avail_width; + state->avail_width = 0; + } + } + else + { + if (state->avail_height > 0) + { + state->children [state->n_children - 1]->alloc.height += state->avail_height; + state->avail_height = 0; + } + } +} + +static void +allocation_stage_allocate (DzlMultiPaned *self, + AllocationState *state) +{ + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (state != NULL); + g_assert (state->children != NULL); + g_assert (state->n_children > 0); + + for (guint i = 0; i < state->n_children; i++) + { + DzlMultiPanedChild *child = state->children [i]; + + gtk_widget_size_allocate (child->widget, &child->alloc); + + if (child->handle != NULL) + { + if (state->n_children != (i + 1)) + { + if (IS_HORIZONTAL (state->orientation)) + { + gdk_window_move_resize (child->handle, + child->alloc.x + child->alloc.width - (HANDLE_WIDTH / 2), + child->alloc.y, + HANDLE_WIDTH, + child->alloc.height); + } + else + { + gdk_window_move_resize (child->handle, + child->alloc.x, + child->alloc.y + child->alloc.height - (HANDLE_HEIGHT / 2), + child->alloc.width, + HANDLE_HEIGHT); + } + + gdk_window_show (child->handle); + } + else + { + gdk_window_hide (child->handle); + } + } + } +} + +static void +dzl_multi_paned_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + DzlMultiPaned *self = (DzlMultiPaned *)widget; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + AllocationState state = { 0 }; + g_autoptr(GPtrArray) children = NULL; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (allocation != NULL); + + GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->size_allocate (widget, allocation); + + if (priv->children->len == 0) + return; + + children = g_ptr_array_new (); + + for (guint i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + child->alloc.x = 0; + child->alloc.y = 0; + child->alloc.width = 0; + child->alloc.height = 0; + + if (child->widget != NULL && + gtk_widget_get_child_visible (child->widget) && + gtk_widget_get_visible (child->widget)) + g_ptr_array_add (children, child); + else if (child->handle) + gdk_window_hide (child->handle); + } + + state.children = (DzlMultiPanedChild **)children->pdata; + state.n_children = children->len; + + if (state.n_children == 0) + return; + + gtk_widget_style_get (GTK_WIDGET (self), + "handle-size", &state.handle_size, + NULL); + + state.orientation = priv->orientation; + state.top_alloc = *allocation; + state.avail_width = allocation->width; + state.avail_height = allocation->height; + + for (guint i = 0; i < G_N_ELEMENTS (allocation_stages); i++) + allocation_stages [i] (self, &state); + + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + +static void +dzl_multi_paned_realize (GtkWidget *widget) +{ + DzlMultiPaned *self = (DzlMultiPaned *)widget; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + + GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->realize (widget); + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + dzl_multi_paned_create_child_handle (self, child); + } +} + +static void +dzl_multi_paned_unrealize (GtkWidget *widget) +{ + DzlMultiPaned *self = (DzlMultiPaned *)widget; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + dzl_multi_paned_destroy_child_handle (self, child); + } + + GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->unrealize (widget); +} + +static void +dzl_multi_paned_map (GtkWidget *widget) +{ + DzlMultiPaned *self = (DzlMultiPaned *)widget; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + + GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->map (widget); + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + gdk_window_show (child->handle); + } +} + +static void +dzl_multi_paned_unmap (GtkWidget *widget) +{ + DzlMultiPaned *self = (DzlMultiPaned *)widget; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + gdk_window_hide (child->handle); + } + + GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->unmap (widget); +} + +static gboolean +dzl_multi_paned_draw (GtkWidget *widget, + cairo_t *cr) +{ + DzlMultiPaned *self = (DzlMultiPaned *)widget; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GtkStyleContext *style_context; + GtkAllocation alloc; + GtkBorder margin; + GtkBorder borders; + GtkStateFlags state; + gint handle_size = 1; + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (cr != NULL); + + gtk_widget_get_allocation (widget, &alloc); + + alloc.x = 0; + alloc.y = 0; + + style_context = gtk_widget_get_style_context (widget); + state = gtk_style_context_get_state (style_context); + + dzl_gtk_style_context_get_borders (style_context, &borders); + + gtk_style_context_get_margin (style_context, state, &margin); + dzl_gtk_allocation_subtract_border (&alloc, &margin); + + gtk_render_background (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height); + + gtk_widget_style_get (widget, "handle-size", &handle_size, NULL); + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + if (!gtk_widget_get_realized (child->widget) || + !gtk_widget_get_visible (child->widget)) + continue; + + gtk_container_propagate_draw (GTK_CONTAINER (self), child->widget, cr); + } + + if (priv->children->len > 0) + { + gtk_style_context_save (style_context); + gtk_style_context_add_class (style_context, "handle"); + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + GtkAllocation child_alloc; + + if (!gtk_widget_get_visible (child->widget) || + !gtk_widget_get_child_visible (child->widget)) + continue; + + if (dzl_multi_paned_is_last_visible_child (self, child)) + break; + + gtk_widget_get_allocation (child->widget, &child_alloc); + gtk_widget_translate_coordinates (child->widget, widget, 0, 0, &child_alloc.x, &child_alloc.y); + + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + gtk_render_handle (style_context, + cr, + child_alloc.x + child_alloc.width, + borders.top, + handle_size, + child_alloc.height); + else + gtk_render_handle (style_context, + cr, + borders.left, + child_alloc.y + child_alloc.height, + child_alloc.width, + handle_size); + + } + + gtk_style_context_restore (style_context); + } + + gtk_render_frame (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height); + + return FALSE; +} + +static void +dzl_multi_paned_pan_gesture_drag_begin (DzlMultiPaned *self, + gdouble x, + gdouble y, + GtkGesturePan *gesture) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GdkEventSequence *sequence; + const GdkEvent *event; + guint i; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (GTK_IS_GESTURE_PAN (gesture)); + g_assert (gesture == priv->gesture); + + sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture)); + event = gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence); + + priv->drag_begin = NULL; + priv->drag_begin_position = 0; + priv->drag_extra_offset = 0; + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + if (child->handle == event->any.window) + { + priv->drag_begin = child; + break; + } + } + + if (priv->drag_begin == NULL) + { + gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED); + return; + } + + for (i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + if (child->handle == event->any.window) + break; + + /* + * We want to make any child before the drag child "sticky" so that it + * will no longer have expand adjustments while we perform the drag + * operation. + */ + if (gtk_widget_get_child_visible (child->widget) && + gtk_widget_get_visible (child->widget)) + { + child->position_set = TRUE; + child->position = IS_HORIZONTAL (priv->orientation) + ? child->alloc.width + : child->alloc.height; + } + } + + if (IS_HORIZONTAL (priv->orientation)) + priv->drag_begin_position = priv->drag_begin->alloc.width; + else + priv->drag_begin_position = priv->drag_begin->alloc.height; + + gtk_gesture_pan_set_orientation (gesture, priv->orientation); + gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); + + g_signal_emit (self, signals [RESIZE_DRAG_BEGIN], 0, priv->drag_begin->widget); +} + +static void +dzl_multi_paned_pan_gesture_drag_end (DzlMultiPaned *self, + gdouble x, + gdouble y, + GtkGesturePan *gesture) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GdkEventSequence *sequence; + GtkEventSequenceState state; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (GTK_IS_GESTURE_PAN (gesture)); + g_assert (gesture == priv->gesture); + + sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture)); + state = gtk_gesture_get_sequence_state (GTK_GESTURE (gesture), sequence); + + if (state != GTK_EVENT_SEQUENCE_CLAIMED) + goto cleanup; + + g_assert (priv->drag_begin != NULL); + + g_signal_emit (self, signals [RESIZE_DRAG_END], 0, priv->drag_begin->widget); + +cleanup: + priv->drag_begin = NULL; + priv->drag_begin_position = 0; + priv->drag_extra_offset = 0; +} + +static void +dzl_multi_paned_pan_gesture_pan (DzlMultiPaned *self, + GtkPanDirection direction, + gdouble offset, + GtkGesturePan *gesture) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GtkAllocation alloc; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (GTK_IS_GESTURE_PAN (gesture)); + g_assert (gesture == priv->gesture); + g_assert (priv->drag_begin != NULL); + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + { + if (direction == GTK_PAN_DIRECTION_LEFT) + offset = -offset; + } + else + { + g_assert (priv->orientation == GTK_ORIENTATION_VERTICAL); + + if (direction == GTK_PAN_DIRECTION_UP) + offset = -offset; + } + + if ((priv->drag_begin_position + offset) < 0) + priv->drag_extra_offset = (priv->drag_begin_position + offset); + else + priv->drag_extra_offset = 0; + + priv->drag_begin->position = MAX (0, priv->drag_begin_position + offset); + priv->drag_begin->position_set = TRUE; + + gtk_widget_queue_allocate (GTK_WIDGET (self)); +} + +static void +dzl_multi_paned_create_pan_gesture (DzlMultiPaned *self) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GtkGesture *gesture; + + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (priv->gesture == NULL); + + gesture = gtk_gesture_pan_new (GTK_WIDGET (self), GTK_ORIENTATION_HORIZONTAL); + gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (gesture), FALSE); + gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture), GTK_PHASE_CAPTURE); + + g_signal_connect_object (gesture, + "drag-begin", + G_CALLBACK (dzl_multi_paned_pan_gesture_drag_begin), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (gesture, + "drag-end", + G_CALLBACK (dzl_multi_paned_pan_gesture_drag_end), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (gesture, + "pan", + G_CALLBACK (dzl_multi_paned_pan_gesture_pan), + self, + G_CONNECT_SWAPPED); + + priv->gesture = GTK_GESTURE_PAN (gesture); +} + +static void +dzl_multi_paned_resize_drag_begin (DzlMultiPaned *self, + GtkWidget *child) +{ + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (GTK_IS_WIDGET (child)); + +} + +static void +dzl_multi_paned_resize_drag_end (DzlMultiPaned *self, + GtkWidget *child) +{ + g_assert (DZL_IS_MULTI_PANED (self)); + g_assert (GTK_IS_WIDGET (child)); + +} + +static void +dzl_multi_paned_get_child_property (GtkContainer *container, + GtkWidget *widget, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlMultiPaned *self = DZL_MULTI_PANED (container); + + switch (prop_id) + { + case CHILD_PROP_INDEX: + g_value_set_int (value, dzl_multi_paned_get_child_index (self, widget)); + break; + + case CHILD_PROP_POSITION: + g_value_set_int (value, dzl_multi_paned_get_child_position (self, widget)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_multi_paned_set_child_property (GtkContainer *container, + GtkWidget *widget, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlMultiPaned *self = DZL_MULTI_PANED (container); + + switch (prop_id) + { + case CHILD_PROP_INDEX: + dzl_multi_paned_set_child_index (self, widget, g_value_get_int (value)); + break; + + case CHILD_PROP_POSITION: + dzl_multi_paned_set_child_position (self, widget, g_value_get_int (value)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_multi_paned_finalize (GObject *object) +{ + DzlMultiPaned *self = (DzlMultiPaned *)object; + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + + g_assert (priv->children->len == 0); + + g_clear_pointer (&priv->children, g_array_unref); + g_clear_object (&priv->gesture); + + G_OBJECT_CLASS (dzl_multi_paned_parent_class)->finalize (object); +} + +static void +dzl_multi_paned_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlMultiPaned *self = DZL_MULTI_PANED (object); + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + + switch (prop_id) + { + case PROP_ORIENTATION: + g_value_set_enum (value, priv->orientation); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_multi_paned_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlMultiPaned *self = DZL_MULTI_PANED (object); + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + + switch (prop_id) + { + case PROP_ORIENTATION: + priv->orientation = g_value_get_enum (value); + for (guint i = 0; i < priv->children->len; i++) + { + DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + child->position_set = FALSE; + } + dzl_multi_paned_update_child_handles (self); + gtk_widget_queue_resize (GTK_WIDGET (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_multi_paned_state_flags_changed (GtkWidget *widget, + GtkStateFlags previous_state) +{ + dzl_multi_paned_update_child_handles (DZL_MULTI_PANED (widget)); + + GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->state_flags_changed (widget, previous_state); +} + +static void +dzl_multi_paned_class_init (DzlMultiPanedClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->get_property = dzl_multi_paned_get_property; + object_class->set_property = dzl_multi_paned_set_property; + object_class->finalize = dzl_multi_paned_finalize; + + widget_class->get_request_mode = dzl_multi_paned_get_request_mode; + widget_class->get_preferred_width = dzl_multi_paned_get_preferred_width; + widget_class->get_preferred_height = dzl_multi_paned_get_preferred_height; + widget_class->get_preferred_width_for_height = dzl_multi_paned_get_preferred_width_for_height; + widget_class->get_preferred_height_for_width = dzl_multi_paned_get_preferred_height_for_width; + widget_class->size_allocate = dzl_multi_paned_size_allocate; + widget_class->realize = dzl_multi_paned_realize; + widget_class->unrealize = dzl_multi_paned_unrealize; + widget_class->map = dzl_multi_paned_map; + widget_class->unmap = dzl_multi_paned_unmap; + widget_class->draw = dzl_multi_paned_draw; + widget_class->state_flags_changed = dzl_multi_paned_state_flags_changed; + + container_class->add = dzl_multi_paned_add; + container_class->remove = dzl_multi_paned_remove; + container_class->get_child_property = dzl_multi_paned_get_child_property; + container_class->set_child_property = dzl_multi_paned_set_child_property; + container_class->forall = dzl_multi_paned_forall; + + klass->resize_drag_begin = dzl_multi_paned_resize_drag_begin; + klass->resize_drag_end = dzl_multi_paned_resize_drag_end; + + gtk_widget_class_set_css_name (widget_class, "dzlmultipaned"); + + properties [PROP_ORIENTATION] = + g_param_spec_enum ("orientation", + "Orientation", + "Orientation", + GTK_TYPE_ORIENTATION, + GTK_ORIENTATION_VERTICAL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + child_properties [CHILD_PROP_INDEX] = + g_param_spec_int ("index", + "Index", + "The index of the child", + -1, + G_MAXINT, + -1, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + child_properties [CHILD_PROP_POSITION] = + g_param_spec_int ("position", + "Position", + "Position", + -1, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + gtk_container_class_install_child_properties (container_class, N_CHILD_PROPS, child_properties); + + style_properties [STYLE_PROP_HANDLE_SIZE] = + g_param_spec_int ("handle-size", + "Handle Size", + "Width of the resize handle", + 0, + G_MAXINT, + 1, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + gtk_widget_class_install_style_property (widget_class, style_properties [STYLE_PROP_HANDLE_SIZE]); + + signals [RESIZE_DRAG_BEGIN] = + g_signal_new ("resize-drag-begin", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlMultiPanedClass, resize_drag_begin), + NULL, NULL, NULL, + G_TYPE_NONE, 1, GTK_TYPE_WIDGET); + + signals [RESIZE_DRAG_END] = + g_signal_new ("resize-drag-end", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlMultiPanedClass, resize_drag_end), + NULL, NULL, NULL, + G_TYPE_NONE, 1, GTK_TYPE_WIDGET); +} + +static void +dzl_multi_paned_init (DzlMultiPaned *self) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + + gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); + + priv->orientation = GTK_ORIENTATION_VERTICAL; + priv->children = g_array_new (FALSE, TRUE, sizeof (DzlMultiPanedChild)); + + dzl_multi_paned_create_pan_gesture (self); +} + +GtkWidget * +dzl_multi_paned_new (void) +{ + return g_object_new (DZL_TYPE_MULTI_PANED, NULL); +} + +guint +dzl_multi_paned_get_n_children (DzlMultiPaned *self) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_MULTI_PANED (self), 0); + + return priv->children ? priv->children->len : 0; +} + +/** + * dzl_multi_paned_get_nth_child: + * @self: a #DzlMultiPaned + * + * Gets the @nth child of the #DzlMultiPaned. + * + * It is an error to call this function with a value >= the result of + * dzl_multi_paned_get_nth_child(). + * + * The index starts from 0. + * + * Returns: (transfer none): A #GtkWidget + */ +GtkWidget * +dzl_multi_paned_get_nth_child (DzlMultiPaned *self, + guint nth) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_MULTI_PANED (self), NULL); + g_return_val_if_fail (nth < priv->children->len, NULL); + + return g_array_index (priv->children, DzlMultiPanedChild, nth).widget; +} + +/** + * dzl_multi_paned_get_at_point: + * @self: a #DzlMultiPaned + * @x: x coordinate + * @y: y coordinate + * + * Locates the widget at position x,y within widget. + * + * @x and @y should be relative to @self. + * + * Returns: (transfer none) (nullable): a #GtkWidget or %NULL + * + * Since: 3.28 + */ +GtkWidget * +dzl_multi_paned_get_at_point (DzlMultiPaned *self, + gint x, + gint y) +{ + DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self); + GtkAllocation alloc; + + g_return_val_if_fail (DZL_IS_MULTI_PANED (self), NULL); + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + if (IS_HORIZONTAL (priv->orientation)) + { + for (guint i = 0; i < priv->children->len; i++) + { + const DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + if (x >= child->alloc.x && x < (child->alloc.x + child->alloc.width)) + return child->widget; + } + } + else + { + for (guint i = 0; i < priv->children->len; i++) + { + const DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i); + + if (y >= child->alloc.y && y < (child->alloc.y + child->alloc.height)) + return child->widget; + } + } + + return NULL; +} diff --git a/src/widgets/dzl-multi-paned.h b/src/widgets/dzl-multi-paned.h new file mode 100644 index 0000000..a7c4f84 --- /dev/null +++ b/src/widgets/dzl-multi-paned.h @@ -0,0 +1,71 @@ +/* dzl-multi-paned.h + * + * Copyright (C) 2016 Christian Hergert + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#if !defined(DAZZLE_INSIDE) && !defined(DAZZLE_COMPILATION) +# error "Only can be included directly." +#endif + +#ifndef DZL_MULTI_PANED_H +#define DZL_MULTI_PANED_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_MULTI_PANED (dzl_multi_paned_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlMultiPaned, dzl_multi_paned, DZL, MULTI_PANED, GtkContainer) + +struct _DzlMultiPanedClass +{ + GtkContainerClass parent; + + void (*resize_drag_begin) (DzlMultiPaned *self, + GtkWidget *child); + void (*resize_drag_end) (DzlMultiPaned *self, + GtkWidget *child); + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_multi_paned_new (void); +DZL_AVAILABLE_IN_ALL +guint dzl_multi_paned_get_n_children (DzlMultiPaned *self); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_multi_paned_get_nth_child (DzlMultiPaned *self, + guint nth); +DZL_AVAILABLE_IN_3_28 +GtkWidget *dzl_multi_paned_get_at_point (DzlMultiPaned *self, + gint x, + gint y); + +G_END_DECLS + +#endif /* DZL_MULTI_PANED_H */ diff --git a/src/widgets/dzl-pill-box.c b/src/widgets/dzl-pill-box.c new file mode 100644 index 0000000..473fc8c --- /dev/null +++ b/src/widgets/dzl-pill-box.c @@ -0,0 +1,133 @@ +/* dzl-pill-box.c + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-pill-box" + +#include "config.h" + +#include + +#include "dzl-pill-box.h" + +struct _DzlPillBox +{ + GtkEventBox parent_instance; + + GtkLabel *label; +}; + +G_DEFINE_TYPE (DzlPillBox, dzl_pill_box, GTK_TYPE_EVENT_BOX) + +enum { + PROP_0, + PROP_LABEL, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +const gchar * +dzl_pill_box_get_label (DzlPillBox *self) +{ + g_return_val_if_fail (DZL_IS_PILL_BOX (self), NULL); + + return gtk_label_get_label (self->label); +} + +void +dzl_pill_box_set_label (DzlPillBox *self, + const gchar *label) +{ + g_return_if_fail (DZL_IS_PILL_BOX (self)); + + gtk_label_set_label (self->label, label); +} + +GtkWidget * +dzl_pill_box_new (const gchar *label) +{ + return g_object_new (DZL_TYPE_PILL_BOX, + "label", label, + NULL); +} + +static void +dzl_pill_box_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPillBox *self = DZL_PILL_BOX (object); + + switch (prop_id) + { + case PROP_LABEL: + g_value_set_string (value, dzl_pill_box_get_label (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_pill_box_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPillBox *self = DZL_PILL_BOX (object); + + switch (prop_id) + { + case PROP_LABEL: + dzl_pill_box_set_label (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_pill_box_class_init (DzlPillBoxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = dzl_pill_box_get_property; + object_class->set_property = dzl_pill_box_set_property; + + properties [PROP_LABEL] = + g_param_spec_string ("label", + "Label", + "The label for the pill box.", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, PROP_LABEL, properties [PROP_LABEL]); + + gtk_widget_class_set_css_name (widget_class, "dzlpillbox"); + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-pill-box.ui"); + gtk_widget_class_bind_template_child (widget_class, DzlPillBox, label); +} + +static void +dzl_pill_box_init (DzlPillBox *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} diff --git a/src/widgets/dzl-pill-box.h b/src/widgets/dzl-pill-box.h new file mode 100644 index 0000000..5074135 --- /dev/null +++ b/src/widgets/dzl-pill-box.h @@ -0,0 +1,43 @@ +/* dzl-pill-box.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PILL_BOX_H +#define DZL_PILL_BOX_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PILL_BOX (dzl_pill_box_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlPillBox, dzl_pill_box, DZL, PILL_BOX, GtkEventBox) + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_pill_box_new (const gchar *label); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_pill_box_get_label (DzlPillBox *self); +DZL_AVAILABLE_IN_ALL +void dzl_pill_box_set_label (DzlPillBox *self, + const gchar *label); + +G_END_DECLS + +#endif /* DZL_PILL_BOX_H */ diff --git a/src/widgets/dzl-pill-box.ui b/src/widgets/dzl-pill-box.ui new file mode 100644 index 0000000..20cce4f --- /dev/null +++ b/src/widgets/dzl-pill-box.ui @@ -0,0 +1,33 @@ + + + + + diff --git a/src/widgets/dzl-priority-box.c b/src/widgets/dzl-priority-box.c new file mode 100644 index 0000000..a2c94a5 --- /dev/null +++ b/src/widgets/dzl-priority-box.c @@ -0,0 +1,268 @@ +/* dzl-priority-box.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +/** + * SECTION:dzlprioritybox: + * @title: DzlPriorityBox + * + * This is like a #GtkBox but uses stable priorities to sort. + */ + +#define G_LOG_DOMAIN "dzl-priority-box" + +#include "config.h" + +#include "dzl-priority-box.h" + +typedef struct +{ + GtkWidget *widget; + gint priority; +} DzlPriorityBoxChild; + +typedef struct +{ + GArray *children; +} DzlPriorityBoxPrivate; + +enum { + CHILD_PROP_0, + CHILD_PROP_PRIORITY, + N_CHILD_PROPS +}; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlPriorityBox, dzl_priority_box, GTK_TYPE_BOX) + +static GParamSpec *child_properties [N_CHILD_PROPS]; + +static gint +sort_by_priority (gconstpointer a, + gconstpointer b) +{ + const DzlPriorityBoxChild *child_a = a; + const DzlPriorityBoxChild *child_b = b; + + return child_a->priority - child_b->priority; +} + +static void +dzl_priority_box_resort (DzlPriorityBox *self) +{ + DzlPriorityBoxPrivate *priv = dzl_priority_box_get_instance_private (self); + guint i; + + g_assert (DZL_IS_PRIORITY_BOX (self)); + + g_array_sort (priv->children, sort_by_priority); + + for (i = 0; i < priv->children->len; i++) + { + DzlPriorityBoxChild *child = &g_array_index (priv->children, DzlPriorityBoxChild, i); + + gtk_container_child_set (GTK_CONTAINER (self), child->widget, + "position", i, + NULL); + } +} + +static gint +dzl_priority_box_get_child_priority (DzlPriorityBox *self, + GtkWidget *widget) +{ + DzlPriorityBoxPrivate *priv = dzl_priority_box_get_instance_private (self); + guint i; + + g_assert (DZL_IS_PRIORITY_BOX (self)); + g_assert (GTK_IS_WIDGET (widget)); + + for (i = 0; i < priv->children->len; i++) + { + DzlPriorityBoxChild *child = &g_array_index (priv->children, DzlPriorityBoxChild, i); + + if (child->widget == widget) + return child->priority; + } + + g_warning ("No such child \"%s\" of \"%s\"", + G_OBJECT_TYPE_NAME (widget), + G_OBJECT_TYPE_NAME (self)); + + return 0; +} + +static void +dzl_priority_box_set_child_priority (DzlPriorityBox *self, + GtkWidget *widget, + gint priority) +{ + DzlPriorityBoxPrivate *priv = dzl_priority_box_get_instance_private (self); + guint i; + + g_assert (DZL_IS_PRIORITY_BOX (self)); + g_assert (GTK_IS_WIDGET (widget)); + + for (i = 0; i < priv->children->len; i++) + { + DzlPriorityBoxChild *child = &g_array_index (priv->children, DzlPriorityBoxChild, i); + + if (child->widget == widget) + { + child->priority = priority; + dzl_priority_box_resort (self); + return; + } + } + + g_warning ("No such child \"%s\" of \"%s\"", + G_OBJECT_TYPE_NAME (widget), + G_OBJECT_TYPE_NAME (self)); +} + +static void +dzl_priority_box_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlPriorityBox *self = (DzlPriorityBox *)container; + DzlPriorityBoxPrivate *priv = dzl_priority_box_get_instance_private (self); + DzlPriorityBoxChild child; + + g_assert (DZL_IS_PRIORITY_BOX (self)); + g_assert (GTK_IS_WIDGET (widget)); + + child.widget = widget; + child.priority = 0; + + g_array_append_val (priv->children, child); + + GTK_CONTAINER_CLASS (dzl_priority_box_parent_class)->add (container, widget); + + dzl_priority_box_resort (self); +} + +static void +dzl_priority_box_remove (GtkContainer *container, + GtkWidget *widget) +{ + DzlPriorityBox *self = (DzlPriorityBox *)container; + DzlPriorityBoxPrivate *priv = dzl_priority_box_get_instance_private (self); + guint i; + + g_assert (DZL_IS_PRIORITY_BOX (self)); + g_assert (GTK_IS_WIDGET (widget)); + + for (i = 0; i < priv->children->len; i++) + { + DzlPriorityBoxChild *child; + + child = &g_array_index (priv->children, DzlPriorityBoxChild, i); + + if (child->widget == widget) + { + g_array_remove_index_fast (priv->children, i); + break; + } + } + + GTK_CONTAINER_CLASS (dzl_priority_box_parent_class)->remove (container, widget); + + dzl_priority_box_resort (self); +} + +static void +dzl_priority_box_finalize (GObject *object) +{ + DzlPriorityBox *self = (DzlPriorityBox *)object; + DzlPriorityBoxPrivate *priv = dzl_priority_box_get_instance_private (self); + + g_clear_pointer (&priv->children, g_array_unref); + + G_OBJECT_CLASS (dzl_priority_box_parent_class)->finalize (object); +} + +static void +dzl_priority_box_get_child_property (GtkContainer *container, + GtkWidget *child, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlPriorityBox *self = DZL_PRIORITY_BOX (container); + + switch (prop_id) + { + case CHILD_PROP_PRIORITY: + g_value_set_int (value, dzl_priority_box_get_child_priority (self, child)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_priority_box_set_child_property (GtkContainer *container, + GtkWidget *child, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlPriorityBox *self = DZL_PRIORITY_BOX (container); + + switch (prop_id) + { + case CHILD_PROP_PRIORITY: + dzl_priority_box_set_child_priority (self, child, g_value_get_int (value)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_priority_box_class_init (DzlPriorityBoxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->finalize = dzl_priority_box_finalize; + + container_class->add = dzl_priority_box_add; + container_class->remove = dzl_priority_box_remove; + container_class->get_child_property = dzl_priority_box_get_child_property; + container_class->set_child_property = dzl_priority_box_set_child_property; + + child_properties [CHILD_PROP_PRIORITY] = + g_param_spec_int ("priority", + "Priority", + "Priority", + G_MININT, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + gtk_container_class_install_child_properties (container_class, N_CHILD_PROPS, child_properties); +} + +static void +dzl_priority_box_init (DzlPriorityBox *self) +{ + DzlPriorityBoxPrivate *priv = dzl_priority_box_get_instance_private (self); + + priv->children = g_array_new (FALSE, FALSE, sizeof (DzlPriorityBoxChild)); +} diff --git a/src/widgets/dzl-priority-box.h b/src/widgets/dzl-priority-box.h new file mode 100644 index 0000000..747d2cc --- /dev/null +++ b/src/widgets/dzl-priority-box.h @@ -0,0 +1,48 @@ +/* dzl-priority-box.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PRIORITY_BOX_H +#define DZL_PRIORITY_BOX_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PRIORITY_BOX (dzl_priority_box_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlPriorityBox, dzl_priority_box, DZL, PRIORITY_BOX, GtkBox) + +struct _DzlPriorityBoxClass +{ + GtkBoxClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_priority_box_new (void); + +G_END_DECLS + +#endif /* DZL_PRIORITY_BOX_H */ diff --git a/src/widgets/dzl-progress-button.c b/src/widgets/dzl-progress-button.c new file mode 100644 index 0000000..ba54db8 --- /dev/null +++ b/src/widgets/dzl-progress-button.c @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2013-2014 Richard Hughes + * + * Licensed under the GNU General Public License Version 2 + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#define G_LOG_DOMAIN "dzl-progress-button" + +#include "config.h" + +#include "widgets/dzl-progress-button.h" +#include "util/dzl-util-private.h" + +typedef struct +{ + GtkButton parent_instance; + guint progress; + guint show_progress : 1; + GtkCssProvider *css_provider; +} DzlProgressButtonPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlProgressButton, dzl_progress_button, GTK_TYPE_BUTTON) + +enum { + PROP_0, + PROP_PROGRESS, + PROP_SHOW_PROGRESS, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +guint +dzl_progress_button_get_progress (DzlProgressButton *self) +{ + DzlProgressButtonPrivate *priv = dzl_progress_button_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_PROGRESS_BUTTON (self), 0); + + return priv->progress; +} + +gboolean +dzl_progress_button_get_show_progress (DzlProgressButton *self) +{ + DzlProgressButtonPrivate *priv = dzl_progress_button_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_PROGRESS_BUTTON (self), FALSE); + + return priv->show_progress; +} + +void +dzl_progress_button_set_progress (DzlProgressButton *button, + guint percentage) +{ + DzlProgressButtonPrivate *priv = dzl_progress_button_get_instance_private (button); + g_autofree gchar *css = NULL; + + g_return_if_fail (DZL_IS_PROGRESS_BUTTON (button)); + + priv->progress = percentage = MIN (percentage, 100); + + if (percentage == 0) + css = g_strdup (".install-progress { background-size: 0; }"); + else if (percentage == 100) + css = g_strdup (".install-progress { background-size: 100%; }"); + else + css = g_strdup_printf (".install-progress { background-size: %u%%; }", percentage); + + gtk_css_provider_load_from_data (priv->css_provider, css, -1, NULL); +} + +void +dzl_progress_button_set_show_progress (DzlProgressButton *button, + gboolean show_progress) +{ + DzlProgressButtonPrivate *priv = dzl_progress_button_get_instance_private (button); + GtkStyleContext *context; + + g_return_if_fail (DZL_IS_PROGRESS_BUTTON (button)); + + priv->show_progress = !!show_progress; + + context = gtk_widget_get_style_context (GTK_WIDGET (button)); + + if (show_progress) + gtk_style_context_add_class (context, "install-progress"); + else + gtk_style_context_remove_class (context, "install-progress"); +} + +static void +dzl_progress_button_dispose (GObject *object) +{ + DzlProgressButton *button = DZL_PROGRESS_BUTTON (object); + DzlProgressButtonPrivate *priv = dzl_progress_button_get_instance_private (button); + + g_clear_object (&priv->css_provider); + + G_OBJECT_CLASS (dzl_progress_button_parent_class)->dispose (object); +} + +static void +dzl_progress_button_init (DzlProgressButton *button) +{ + DzlProgressButtonPrivate *priv = dzl_progress_button_get_instance_private (button); + + priv->css_provider = gtk_css_provider_new (); + + gtk_style_context_add_provider (gtk_widget_get_style_context (GTK_WIDGET (button)), + GTK_STYLE_PROVIDER (priv->css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +static void +dzl_progress_button_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlProgressButton *self = DZL_PROGRESS_BUTTON (object); + + switch (prop_id) + { + case PROP_PROGRESS: + g_value_set_uint (value, dzl_progress_button_get_progress (self)); + break; + + case PROP_SHOW_PROGRESS: + g_value_set_boolean (value, dzl_progress_button_get_show_progress (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_progress_button_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlProgressButton *self = DZL_PROGRESS_BUTTON (object); + + switch (prop_id) + { + case PROP_PROGRESS: + dzl_progress_button_set_progress (self, g_value_get_uint (value)); + break; + + case PROP_SHOW_PROGRESS: + dzl_progress_button_set_show_progress (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_progress_button_class_init (DzlProgressButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = dzl_progress_button_dispose; + object_class->get_property = dzl_progress_button_get_property; + object_class->set_property = dzl_progress_button_set_property; + + properties [PROP_PROGRESS] = + g_param_spec_uint ("progress", NULL, NULL, 0, 100, 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_PROGRESS] = + g_param_spec_boolean ("show-progress", NULL, NULL, FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +GtkWidget * +dzl_progress_button_new (void) +{ + return g_object_new (DZL_TYPE_PROGRESS_BUTTON, NULL); +} diff --git a/src/widgets/dzl-progress-button.h b/src/widgets/dzl-progress-button.h new file mode 100644 index 0000000..03f1a7b --- /dev/null +++ b/src/widgets/dzl-progress-button.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013-2014 Richard Hughes + * + * Licensed under the GNU General Public License Version 2 + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef DZL_PROGRESS_BUTTON_H +#define DZL_PROGRESS_BUTTON_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PROGRESS_BUTTON (dzl_progress_button_get_type ()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlProgressButton, dzl_progress_button, DZL, PROGRESS_BUTTON, GtkButton) + +struct _DzlProgressButtonClass +{ + GtkButtonClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_progress_button_new (void); +DZL_AVAILABLE_IN_ALL +guint dzl_progress_button_get_progress (DzlProgressButton *self); +DZL_AVAILABLE_IN_ALL +void dzl_progress_button_set_progress (DzlProgressButton *button, + guint percentage); +DZL_AVAILABLE_IN_ALL +gboolean dzl_progress_button_get_show_progress (DzlProgressButton *self); +DZL_AVAILABLE_IN_ALL +void dzl_progress_button_set_show_progress (DzlProgressButton *button, + gboolean show_progress); + +G_END_DECLS + +#endif /* DZL_PROGRESS_BUTTON_H */ diff --git a/src/widgets/dzl-progress-icon.c b/src/widgets/dzl-progress-icon.c new file mode 100644 index 0000000..1baf488 --- /dev/null +++ b/src/widgets/dzl-progress-icon.c @@ -0,0 +1,197 @@ +/* dzl-progress-icon.c + * + * Copyright (C) 2015 Igalia S.L. + * Copyright (C) 2016-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-progress-icon" + +#include "config.h" + +#include + +#include "dzl-progress-icon.h" + +struct _DzlProgressIcon +{ + GtkDrawingArea parent_instance; + gdouble progress; +}; + +G_DEFINE_TYPE (DzlProgressIcon, dzl_progress_icon, GTK_TYPE_DRAWING_AREA) + +enum { + PROP_0, + PROP_PROGRESS, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static gboolean +dzl_progress_icon_draw (GtkWidget *widget, + cairo_t *cr) +{ + DzlProgressIcon *self = (DzlProgressIcon *)widget; + GtkStyleContext *style_context; + GtkAllocation alloc; + GdkRGBA rgba; + GtkStateFlags state; + gdouble alpha; + + g_assert (DZL_IS_PROGRESS_ICON (self)); + g_assert (cr != NULL); + + gtk_widget_get_allocation (widget, &alloc); + + style_context = gtk_widget_get_style_context (widget); + state = gtk_style_context_get_state (style_context); + gtk_style_context_get_color (style_context, state, &rgba); + + alpha = rgba.alpha; + rgba.alpha = 0.15; + gdk_cairo_set_source_rgba (cr, &rgba); + + cairo_arc (cr, + alloc.width / 2, + alloc.height / 2, + alloc.width / 2, + 0.0, + 2 * M_PI); + cairo_fill (cr); + + if (self->progress > 0.0) + { + rgba.alpha = alpha; + gdk_cairo_set_source_rgba (cr, &rgba); + + cairo_arc (cr, + alloc.width / 2, + alloc.height / 2, + alloc.width / 2, + (-.5 * M_PI), + (2 * self->progress * M_PI) - (.5 * M_PI)); + + if (self->progress != 1.0) + { + cairo_line_to (cr, alloc.width / 2, alloc.height / 2); + cairo_line_to (cr, alloc.width / 2, 0); + } + + cairo_fill (cr); + } + + return GDK_EVENT_PROPAGATE; +} + +static void +dzl_progress_icon_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlProgressIcon *self = DZL_PROGRESS_ICON (object); + + switch (prop_id) + { + case PROP_PROGRESS: + g_value_set_double (value, dzl_progress_icon_get_progress (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_progress_icon_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlProgressIcon *self = DZL_PROGRESS_ICON (object); + + switch (prop_id) + { + case PROP_PROGRESS: + dzl_progress_icon_set_progress (self, g_value_get_double (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_progress_icon_class_init (DzlProgressIconClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = dzl_progress_icon_get_property; + object_class->set_property = dzl_progress_icon_set_property; + + widget_class->draw = dzl_progress_icon_draw; + + properties [PROP_PROGRESS] = + g_param_spec_double ("progress", + "Progress", + "Progress", + 0.0, + 1.0, + 0.0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_progress_icon_init (DzlProgressIcon *icon) +{ + g_object_set (icon, "width-request", 16, "height-request", 16, NULL); + gtk_widget_set_valign (GTK_WIDGET (icon), GTK_ALIGN_CENTER); + gtk_widget_set_halign (GTK_WIDGET (icon), GTK_ALIGN_CENTER); +} + +GtkWidget * +dzl_progress_icon_new (void) +{ + return g_object_new (DZL_TYPE_PROGRESS_ICON, NULL); +} + +gdouble +dzl_progress_icon_get_progress (DzlProgressIcon *self) +{ + g_return_val_if_fail (DZL_IS_PROGRESS_ICON (self), 0.0); + + return self->progress; +} + +void +dzl_progress_icon_set_progress (DzlProgressIcon *self, + gdouble progress) +{ + g_return_if_fail (DZL_IS_PROGRESS_ICON (self)); + + progress = CLAMP (progress, 0.0, 1.0); + + if (self->progress != progress) + { + self->progress = progress; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]); + gtk_widget_queue_draw (GTK_WIDGET (self)); + } +} diff --git a/src/widgets/dzl-progress-icon.h b/src/widgets/dzl-progress-icon.h new file mode 100644 index 0000000..e195400 --- /dev/null +++ b/src/widgets/dzl-progress-icon.h @@ -0,0 +1,43 @@ +/* dzl-progress-icon.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PROGRESS_ICON_H +#define DZL_PROGRESS_ICON_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PROGRESS_ICON (dzl_progress_icon_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlProgressIcon, dzl_progress_icon, DZL, PROGRESS_ICON, GtkDrawingArea) + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_progress_icon_new (void); +DZL_AVAILABLE_IN_ALL +gdouble dzl_progress_icon_get_progress (DzlProgressIcon *self); +DZL_AVAILABLE_IN_ALL +void dzl_progress_icon_set_progress (DzlProgressIcon *self, + gdouble progress); + +G_END_DECLS + +#endif /* DZL_PROGRESS_ICON_H */ diff --git a/src/widgets/dzl-progress-menu-button.c b/src/widgets/dzl-progress-menu-button.c new file mode 100644 index 0000000..c68be0c --- /dev/null +++ b/src/widgets/dzl-progress-menu-button.c @@ -0,0 +1,331 @@ +/* dzl-progress-menu-button.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-progress-menu-button" + +#include "config.h" + +#include "animation/dzl-animation.h" +#include "animation/dzl-box-theatric.h" +#include "widgets/dzl-progress-icon.h" +#include "widgets/dzl-progress-menu-button.h" + +typedef struct +{ + GtkMenuButton parent_instance; + DzlProgressIcon *icon; + const gchar *theatric_icon_name; + gdouble progress; + guint transition_duration; + guint show_theatric : 1; + guint suppress_theatric : 1; +} DzlProgressMenuButtonPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlProgressMenuButton, dzl_progress_menu_button, GTK_TYPE_MENU_BUTTON) + +enum { + PROP_0, + PROP_PROGRESS, + PROP_SHOW_THEATRIC, + PROP_THEATRIC_ICON_NAME, + PROP_TRANSITION_DURATION, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static void dzl_progress_menu_button_begin_theatrics (DzlProgressMenuButton *self); + +GtkWidget * +dzl_progress_menu_button_new (void) +{ + return g_object_new (DZL_TYPE_PROGRESS_MENU_BUTTON, NULL); +} + +gdouble +dzl_progress_menu_button_get_progress (DzlProgressMenuButton *self) +{ + DzlProgressMenuButtonPrivate *priv = dzl_progress_menu_button_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_PROGRESS_MENU_BUTTON (self), 0.0); + + return priv->progress; +} + +void +dzl_progress_menu_button_set_progress (DzlProgressMenuButton *self, + gdouble progress) +{ + DzlProgressMenuButtonPrivate *priv = dzl_progress_menu_button_get_instance_private (self); + + g_return_if_fail (DZL_IS_PROGRESS_MENU_BUTTON (self)); + g_return_if_fail (progress >= 0.0); + g_return_if_fail (progress <= 1.0); + + if (progress != priv->progress) + { + priv->progress = progress; + dzl_progress_icon_set_progress (priv->icon, progress); + if (progress == 1.0) + dzl_progress_menu_button_begin_theatrics (self); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]); + } +} + +gboolean +dzl_progress_menu_button_get_show_theatric (DzlProgressMenuButton *self) +{ + DzlProgressMenuButtonPrivate *priv = dzl_progress_menu_button_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_PROGRESS_MENU_BUTTON (self), FALSE); + + return priv->show_theatric; +} + +void +dzl_progress_menu_button_set_show_theatric (DzlProgressMenuButton *self, + gboolean show_theatric) +{ + DzlProgressMenuButtonPrivate *priv = dzl_progress_menu_button_get_instance_private (self); + + g_return_if_fail (DZL_IS_PROGRESS_MENU_BUTTON (self)); + + show_theatric = !!show_theatric; + + if (priv->show_theatric != show_theatric) + { + priv->show_theatric = show_theatric; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_THEATRIC]); + } +} + +static gboolean +begin_theatrics_from_main (gpointer user_data) +{ + DzlProgressMenuButton *self = user_data; + GtkAllocation rect; + + g_assert (DZL_IS_PROGRESS_MENU_BUTTON (self)); + + /* Ignore if still ont allocated */ + gtk_widget_get_allocation (GTK_WIDGET (self), &rect); + if (rect.x != -1 && rect.y != -1) + dzl_progress_menu_button_begin_theatrics (self); + + return G_SOURCE_REMOVE; +} + +static void +dzl_progress_menu_button_begin_theatrics (DzlProgressMenuButton *self) +{ + DzlProgressMenuButtonPrivate *priv = dzl_progress_menu_button_get_instance_private (self); + g_autoptr(GIcon) icon = NULL; + DzlBoxTheatric *theatric; + GtkAllocation rect; + + g_assert (DZL_IS_PROGRESS_MENU_BUTTON (self)); + + if (!priv->show_theatric || priv->transition_duration == 0 || priv->suppress_theatric) + return; + + gtk_widget_get_allocation (GTK_WIDGET (self), &rect); + + if (rect.x == -1 && rect.y == -1) + { + /* Delay this until our widget has been mapped/realized/displayed */ + gdk_threads_add_idle_full (G_PRIORITY_LOW, + begin_theatrics_from_main, + g_object_ref (self), g_object_unref); + return; + } + + rect.x = 0; + rect.y = 0; + + icon = g_themed_icon_new (priv->theatric_icon_name); + + theatric = g_object_new (DZL_TYPE_BOX_THEATRIC, + "alpha", 1.0, + "height", rect.height, + "icon", icon, + "target", self, + "width", rect.width, + "x", rect.x, + "y", rect.y, + NULL); + + dzl_object_animate_full (theatric, + DZL_ANIMATION_EASE_OUT_CUBIC, + priv->transition_duration, + gtk_widget_get_frame_clock (GTK_WIDGET (self)), + g_object_unref, + theatric, + "x", rect.x - 60, + "width", rect.width + 120, + "y", rect.y, + "height", rect.height + 120, + "alpha", 0.0, + NULL); + + priv->suppress_theatric = TRUE; +} + +static void +dzl_progress_menu_button_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlProgressMenuButton *self = DZL_PROGRESS_MENU_BUTTON (object); + DzlProgressMenuButtonPrivate *priv = dzl_progress_menu_button_get_instance_private (self); + + switch (prop_id) + { + case PROP_PROGRESS: + g_value_set_double (value, priv->progress); + break; + + case PROP_SHOW_THEATRIC: + g_value_set_boolean (value, priv->show_theatric); + break; + + case PROP_THEATRIC_ICON_NAME: + g_value_set_static_string (value, priv->theatric_icon_name); + break; + + case PROP_TRANSITION_DURATION: + g_value_set_uint (value, priv->transition_duration); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_progress_menu_button_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlProgressMenuButton *self = DZL_PROGRESS_MENU_BUTTON (object); + DzlProgressMenuButtonPrivate *priv = dzl_progress_menu_button_get_instance_private (self); + + switch (prop_id) + { + case PROP_PROGRESS: + dzl_progress_menu_button_set_progress (self, g_value_get_double (value)); + break; + + case PROP_SHOW_THEATRIC: + dzl_progress_menu_button_set_show_theatric (self, g_value_get_double (value)); + break; + + case PROP_THEATRIC_ICON_NAME: + priv->theatric_icon_name = g_intern_string (g_value_get_string (value)); + break; + + case PROP_TRANSITION_DURATION: + priv->transition_duration = g_value_get_uint (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_progress_menu_button_class_init (DzlProgressMenuButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = dzl_progress_menu_button_get_property; + object_class->set_property = dzl_progress_menu_button_set_property; + + properties [PROP_PROGRESS] = + g_param_spec_double ("progress", + "Progress", + "Progress", + 0.0, 1.0, 0.0, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_THEATRIC] = + g_param_spec_boolean ("show-theatric", + "Show Theatric", + "Show Theatric", + TRUE, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + properties [PROP_THEATRIC_ICON_NAME] = + g_param_spec_string ("theatric-icon-name", + "Theatric Icon Name", + "Theatric Icon Name", + "folder-download-symbolic", + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TRANSITION_DURATION] = + g_param_spec_uint ("transition-duration", + "Transition Duration", + "Transition Duration", + 0, + 5000, + 750, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_progress_menu_button_init (DzlProgressMenuButton *self) +{ + DzlProgressMenuButtonPrivate *priv = dzl_progress_menu_button_get_instance_private (self); + + priv->theatric_icon_name = g_intern_static_string ("folder-download-symbolic"); + priv->show_theatric = TRUE; + priv->transition_duration = 750; + + priv->icon = g_object_new (DZL_TYPE_PROGRESS_ICON, + "visible", TRUE, + NULL); + g_signal_connect (priv->icon, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &priv->icon); + gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (priv->icon)); +} + +/** + * dzl_progress_menu_button_reset_theatrics: + * @self: a #DzlProgressMenuButton + * + * To avoid suprious animations from the button, you must call this function any + * time you want to allow animations to continue. This is because animations are + * automatically started upon reaching a progress of 1.0. + * + * If you are performing operations in the background, calling this function + * every time you add an operation is a good strategry. + */ +void +dzl_progress_menu_button_reset_theatrics (DzlProgressMenuButton *self) +{ + DzlProgressMenuButtonPrivate *priv = dzl_progress_menu_button_get_instance_private (self); + + g_return_if_fail (DZL_IS_PROGRESS_MENU_BUTTON (self)); + + priv->suppress_theatric = FALSE; +} diff --git a/src/widgets/dzl-progress-menu-button.h b/src/widgets/dzl-progress-menu-button.h new file mode 100644 index 0000000..c3ef499 --- /dev/null +++ b/src/widgets/dzl-progress-menu-button.h @@ -0,0 +1,61 @@ +/* dzl-progress-menu-button.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_PROGRESS_MENU_BUTTON_H +#define DZL_PROGRESS_MENU_BUTTON_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_PROGRESS_MENU_BUTTON (dzl_progress_menu_button_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlProgressMenuButton, dzl_progress_menu_button, DZL, PROGRESS_MENU_BUTTON, GtkMenuButton) + +struct _DzlProgressMenuButtonClass +{ + GtkMenuButtonClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_progress_menu_button_new (void); +DZL_AVAILABLE_IN_ALL +gdouble dzl_progress_menu_button_get_progress (DzlProgressMenuButton *button); +DZL_AVAILABLE_IN_ALL +void dzl_progress_menu_button_set_progress (DzlProgressMenuButton *button, + gdouble progress); +DZL_AVAILABLE_IN_ALL +gboolean dzl_progress_menu_button_get_show_theatric (DzlProgressMenuButton *self); +DZL_AVAILABLE_IN_ALL +void dzl_progress_menu_button_set_show_theatric (DzlProgressMenuButton *self, + gboolean show_theatic); +DZL_AVAILABLE_IN_ALL +void dzl_progress_menu_button_reset_theatrics (DzlProgressMenuButton *self); + +G_END_DECLS + +#endif /* DZL_PROGRESS_MENU_BUTTON_H */ + diff --git a/src/widgets/dzl-radio-box.c b/src/widgets/dzl-radio-box.c new file mode 100644 index 0000000..2a38b41 --- /dev/null +++ b/src/widgets/dzl-radio-box.c @@ -0,0 +1,512 @@ +/* dzl-radio-box.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-radio-box" + +#include "config.h" + +#include "dzl-radio-box.h" + +/* + * XXX: Ideally we would manage all the size requests ourselves. However, + * that takes some more work to do correctly (and support stuff like + * linked, etc). + */ +#define N_PER_ROW 4 + +typedef struct +{ + gchar *id; + gchar *text; + GtkToggleButton *button; +} DzlRadioBoxItem; + +typedef struct +{ + GArray *items; + gchar *active_id; + + GtkBox *vbox; + GtkBox *hbox; + GtkRevealer *revealer; + + guint has_more : 1; +} DzlRadioBoxPrivate; + +static void buildable_iface_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_EXTENDED (DzlRadioBox, dzl_radio_box, GTK_TYPE_BIN, 0, + G_ADD_PRIVATE (DzlRadioBox) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init)) + +enum { + PROP_0, + PROP_ACTIVE_ID, + PROP_HAS_MORE, + PROP_SHOW_MORE, + N_PROPS +}; + +enum { + CHANGED, + N_SIGNALS +}; + +static GParamSpec *properties [N_PROPS]; +static guint signals [N_SIGNALS]; + +static gboolean +dzl_radio_box_get_has_more (DzlRadioBox *self) +{ + DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_RADIO_BOX (self), FALSE); + + return priv->has_more; +} + +static gboolean +dzl_radio_box_get_show_more (DzlRadioBox *self) +{ + DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_RADIO_BOX (self), FALSE); + + return gtk_revealer_get_reveal_child (priv->revealer); +} + +static void +dzl_radio_box_set_show_more (DzlRadioBox *self, + gboolean show_more) +{ + DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self); + + g_return_if_fail (DZL_IS_RADIO_BOX (self)); + + gtk_revealer_set_reveal_child (priv->revealer, show_more); +} + +static void +dzl_radio_box_item_clear (DzlRadioBoxItem *item) +{ + g_free (item->id); + g_free (item->text); +} + +static void +dzl_radio_box_finalize (GObject *object) +{ + DzlRadioBox *self = (DzlRadioBox *)object; + DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self); + + g_clear_pointer (&priv->items, g_array_unref); + g_clear_pointer (&priv->active_id, g_free); + + G_OBJECT_CLASS (dzl_radio_box_parent_class)->finalize (object); +} + +static void +dzl_radio_box_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlRadioBox *self = DZL_RADIO_BOX (object); + + switch (prop_id) + { + case PROP_ACTIVE_ID: + g_value_set_string (value, dzl_radio_box_get_active_id (self)); + break; + + case PROP_HAS_MORE: + g_value_set_boolean (value, dzl_radio_box_get_has_more (self)); + break; + + case PROP_SHOW_MORE: + g_value_set_boolean (value, dzl_radio_box_get_show_more (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_radio_box_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlRadioBox *self = DZL_RADIO_BOX (object); + + switch (prop_id) + { + case PROP_ACTIVE_ID: + dzl_radio_box_set_active_id (self, g_value_get_string (value)); + break; + + case PROP_SHOW_MORE: + dzl_radio_box_set_show_more (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_radio_box_class_init (DzlRadioBoxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_radio_box_finalize; + object_class->get_property = dzl_radio_box_get_property; + object_class->set_property = dzl_radio_box_set_property; + + properties [PROP_ACTIVE_ID] = + g_param_spec_string ("active-id", + "Active Id", + "Active Id", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_HAS_MORE] = + g_param_spec_boolean ("has-more", + "Has More", + "Has more items to view", + FALSE, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_MORE] = + g_param_spec_boolean ("show-more", + "Show More", + "Show additional items", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + signals [CHANGED] = + g_signal_new ("changed", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, G_TYPE_NONE, 0); + + gtk_widget_class_set_css_name (widget_class, "radiobox"); +} + +static void +dzl_radio_box_init (DzlRadioBox *self) +{ + DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self); + g_autoptr(GSimpleActionGroup) group = g_simple_action_group_new (); + g_autoptr(GPropertyAction) action = NULL; + GtkWidget *vbox; + + /* GPropertyAction doesn't like NULL strings */ + priv->active_id = g_strdup (""); + + priv->items = g_array_new (FALSE, FALSE, sizeof (DzlRadioBoxItem)); + g_array_set_clear_func (priv->items, (GDestroyNotify)dzl_radio_box_item_clear); + + vbox = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_VERTICAL, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (vbox)); + + priv->hbox = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "visible", TRUE, + NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->hbox)), "linked"); + gtk_container_add (GTK_CONTAINER (vbox), GTK_WIDGET (priv->hbox)); + + priv->revealer = g_object_new (GTK_TYPE_REVEALER, + "reveal-child", FALSE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (vbox), GTK_WIDGET (priv->revealer)); + + priv->vbox = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_VERTICAL, + "margin-top", 12, + "spacing", 12, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (priv->revealer), GTK_WIDGET (priv->vbox)); + + action = g_property_action_new ("active", self, "active-id"); + g_action_map_add_action (G_ACTION_MAP (group), G_ACTION (action)); + gtk_widget_insert_action_group (GTK_WIDGET (self), "radiobox", G_ACTION_GROUP (group)); +} + +void +dzl_radio_box_add_item (DzlRadioBox *self, + const gchar *id, + const gchar *text) +{ + DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self); + DzlRadioBoxItem item = { 0 }; + guint precount; + + g_return_if_fail (DZL_IS_RADIO_BOX (self)); + g_return_if_fail (id != NULL); + g_return_if_fail (text != NULL); + + precount = priv->items->len; + + for (guint i = 0; i < precount; ++i) + { + /* Avoid duplicate items */ + if (!g_strcmp0 (g_array_index (priv->items, DzlRadioBoxItem, i).id, id)) + return; + } + + item.id = g_strdup (id); + item.text = g_strdup (text); + item.button = g_object_new (GTK_TYPE_TOGGLE_BUTTON, + "active", (g_strcmp0 (id, priv->active_id) == 0), + "action-name", "radiobox.active", + "action-target", g_variant_new_string (id), + "label", text, + "visible", TRUE, + NULL); + + g_array_append_val (priv->items, item); + + if (precount > 0 && (precount % N_PER_ROW) == 0) + { + gboolean show_more = dzl_radio_box_get_show_more (self); + gboolean visible = !priv->has_more || show_more; + + priv->has_more = priv->items->len > N_PER_ROW; + priv->hbox = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "visible", visible, + NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->hbox)), "linked"); + gtk_container_add (GTK_CONTAINER (priv->vbox), GTK_WIDGET (priv->hbox)); + } + + gtk_container_add_with_properties (GTK_CONTAINER (priv->hbox), GTK_WIDGET (item.button), + "expand", TRUE, + NULL); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_MORE]); + + /* If this is the first item and no active id has been set, + * then go ahead and set the active item to this one. + */ + if (priv->items->len == 1 && (!priv->active_id || !*priv->active_id)) + dzl_radio_box_set_active_id (self, id); +} + +void +dzl_radio_box_set_active_id (DzlRadioBox *self, + const gchar *id) +{ + DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self); + + g_return_if_fail (DZL_IS_RADIO_BOX (self)); + + if (id == NULL) + id = ""; + + if (g_strcmp0 (id, priv->active_id) != 0) + { + g_free (priv->active_id); + priv->active_id = g_strdup (id); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVE_ID]); + g_signal_emit (self, signals [CHANGED], 0); + } +} + +const gchar * +dzl_radio_box_get_active_id (DzlRadioBox *self) +{ + DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_RADIO_BOX (self), NULL); + + return priv->active_id; +} + +GtkWidget * +dzl_radio_box_new (void) +{ + return g_object_new (DZL_TYPE_RADIO_BOX, NULL); +} + +typedef struct +{ + DzlRadioBox *self; + GtkBuilder *builder; + gchar *id; + GString *text; + guint translatable : 1; +} ItemParserData; + +static void +item_parser_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + ItemParserData *parser_data = user_data; + + g_assert (context != NULL); + g_assert (element_name != NULL); + g_assert (parser_data != NULL); + + if (g_strcmp0 (element_name, "item") == 0) + { + const gchar *translatable = NULL; + + g_clear_pointer (&parser_data->id, g_free); + g_string_truncate (parser_data->text, 0); + + if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error, + G_MARKUP_COLLECT_STRDUP, "id", &parser_data->id, + G_MARKUP_COLLECT_STRING, "translatable", &translatable, + G_MARKUP_COLLECT_INVALID)) + return; + + parser_data->translatable = translatable != NULL && g_str_equal ("yes", translatable); + } +} + +static void +item_parser_end_element (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + ItemParserData *parser_data = user_data; + + g_assert (context != NULL); + g_assert (element_name != NULL); + g_assert (parser_data != NULL); + + if (g_strcmp0 (element_name, "item") == 0) + { + if (parser_data->id && parser_data->text != NULL) + { + const gchar *str = parser_data->text->str; + + if (parser_data->translatable && str != NULL) + { + const gchar *domain; + + domain = gtk_builder_get_translation_domain (parser_data->builder); + str = g_dgettext (domain, str); + } + + dzl_radio_box_add_item (parser_data->self, parser_data->id, str); + } + } +} + +static void +item_parser_text (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + ItemParserData *parser_data = user_data; + + g_assert (parser_data != NULL); + + if (parser_data->text == NULL) + parser_data->text = g_string_new (NULL); + + g_string_append_len (parser_data->text, text, text_len); +} + +static GMarkupParser ItemParser = { + item_parser_start_element, + item_parser_end_element, + item_parser_text, +}; + +static gboolean +dzl_radio_box_custom_tag_start (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *tagname, + GMarkupParser *parser, + gpointer *data) +{ + DzlRadioBox *self = (DzlRadioBox *)buildable; + + g_assert (DZL_IS_RADIO_BOX (self)); + g_assert (GTK_IS_BUILDER (builder)); + g_assert (tagname != NULL); + g_assert (parser != NULL); + g_assert (data != NULL); + + if (g_strcmp0 (tagname, "items") == 0) + { + ItemParserData *parser_data; + + parser_data = g_slice_new0 (ItemParserData); + parser_data->self = self; + parser_data->builder = builder; + + *parser = ItemParser; + *data = parser_data; + + return TRUE; + } + + return FALSE; +} + +static void +dzl_radio_box_custom_finished (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *tagname, + gpointer user_data) +{ + DzlRadioBox *self = (DzlRadioBox *)buildable; + + g_assert (DZL_IS_RADIO_BOX (self)); + g_assert (GTK_IS_BUILDER (builder)); + g_assert (tagname != NULL); + + if (g_strcmp0 (tagname, "items") == 0) + { + ItemParserData *parser_data = user_data; + + g_free (parser_data->id); + g_string_free (parser_data->text, TRUE); + g_slice_free (ItemParserData, parser_data); + } +} + +static void +buildable_iface_init (GtkBuildableIface *iface) +{ + iface->custom_tag_start = dzl_radio_box_custom_tag_start; + iface->custom_finished = dzl_radio_box_custom_finished; +} diff --git a/src/widgets/dzl-radio-box.h b/src/widgets/dzl-radio-box.h new file mode 100644 index 0000000..0ce3d8c --- /dev/null +++ b/src/widgets/dzl-radio-box.h @@ -0,0 +1,57 @@ +/* dzl-radio-box.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_RADIO_BOX_H +#define DZL_RADIO_BOX_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_RADIO_BOX (dzl_radio_box_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlRadioBox, dzl_radio_box, DZL, RADIO_BOX, GtkBin) + +struct _DzlRadioBoxClass +{ + GtkBinClass parent_class; + + gpointer _padding1; + gpointer _padding2; + gpointer _padding3; + gpointer _padding4; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_radio_box_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_radio_box_add_item (DzlRadioBox *self, + const gchar *id, + const gchar *text); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_radio_box_get_active_id (DzlRadioBox *self); +DZL_AVAILABLE_IN_ALL +void dzl_radio_box_set_active_id (DzlRadioBox *self, + const gchar *id); + +G_END_DECLS + +#endif /* DZL_RADIO_BOX_H */ diff --git a/src/widgets/dzl-rect-helper.c b/src/widgets/dzl-rect-helper.c new file mode 100644 index 0000000..fba36d0 --- /dev/null +++ b/src/widgets/dzl-rect-helper.c @@ -0,0 +1,175 @@ +/* dzl-rect-helper.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-rect-helper" + +#include "config.h" + +#include + +#include "widgets/dzl-rect-helper.h" + +struct _DzlRectHelper +{ + GObject parent_instance; + + gint x; + gint y; + gint width; + gint height; +}; + +enum { + PROP_0, + PROP_X, + PROP_Y, + PROP_WIDTH, + PROP_HEIGHT, + LAST_PROP +}; + +G_DEFINE_TYPE (DzlRectHelper, dzl_rect_helper, G_TYPE_OBJECT) + +static GParamSpec *properties [LAST_PROP]; + +static void +dzl_rect_helper_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlRectHelper *self = DZL_RECT_HELPER (object); + + switch (prop_id) + { + case PROP_X: + g_value_set_int (value, self->x); + break; + + case PROP_Y: + g_value_set_int (value, self->y); + break; + + case PROP_WIDTH: + g_value_set_int (value, self->width); + break; + + case PROP_HEIGHT: + g_value_set_int (value, self->height); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_rect_helper_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlRectHelper *self = DZL_RECT_HELPER (object); + + switch (prop_id) + { + case PROP_X: + self->x = g_value_get_int (value); + break; + + case PROP_Y: + self->y = g_value_get_int (value); + break; + + case PROP_WIDTH: + self->width = g_value_get_int (value); + break; + + case PROP_HEIGHT: + self->height = g_value_get_int (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_rect_helper_class_init (DzlRectHelperClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = dzl_rect_helper_get_property; + object_class->set_property = dzl_rect_helper_set_property; + + properties [PROP_X] = + g_param_spec_int ("x", + "X", + "X", + G_MININT, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_Y] = + g_param_spec_int ("y", + "Y", + "Y", + G_MININT, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_WIDTH] = + g_param_spec_int ("width", + "Width", + "Width", + G_MININT, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_HEIGHT] = + g_param_spec_int ("height", + "Height", + "Height", + G_MININT, + G_MAXINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); +} + +static void +dzl_rect_helper_init (DzlRectHelper *rect) +{ +} + +void +dzl_rect_helper_get_rect (DzlRectHelper *self, + GdkRectangle *rect) +{ + g_return_if_fail (DZL_IS_RECT_HELPER (self)); + g_return_if_fail (rect != NULL); + + rect->x = self->x; + rect->y = self->y; + rect->width = self->width; + rect->height = self->height; +} diff --git a/src/widgets/dzl-rect-helper.h b/src/widgets/dzl-rect-helper.h new file mode 100644 index 0000000..ec52f46 --- /dev/null +++ b/src/widgets/dzl-rect-helper.h @@ -0,0 +1,41 @@ +/* dzl-rect-helper.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_RECT_HELPER_H +#define DZL_RECT_HELPER_H + +#include + +/* + * This is just a helper object for animating rectangles. + * It allows us to use dzl_object_animate() to animate + * coordinates. + */ + +G_BEGIN_DECLS + +#define DZL_TYPE_RECT_HELPER (dzl_rect_helper_get_type()) + +G_DECLARE_FINAL_TYPE (DzlRectHelper, dzl_rect_helper, DZL, RECT_HELPER, GObject) + +void dzl_rect_helper_get_rect (DzlRectHelper *self, + GdkRectangle *rect); + +G_END_DECLS + +#endif /* DZL_RECT_HELPER_H */ diff --git a/src/widgets/dzl-scrolled-window.c b/src/widgets/dzl-scrolled-window.c new file mode 100644 index 0000000..1d6e7da --- /dev/null +++ b/src/widgets/dzl-scrolled-window.c @@ -0,0 +1,103 @@ +/* dzl-scrolled-window.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-scrolled-window" + +#include "config.h" + +#include "dzl-scrolled-window.h" + +struct _DzlScrolledWindow +{ + GtkScrolledWindow parent_instance; +}; + +G_DEFINE_TYPE (DzlScrolledWindow, dzl_scrolled_window, GTK_TYPE_SCROLLED_WINDOW) + +static void +dzl_scrolled_window_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *min_height, + gint *nat_height) +{ + DzlScrolledWindow *self = (DzlScrolledWindow *)widget; + gint border_width; + gint min_content_height; + gint max_content_height; + GtkWidget *child; + + g_assert (DZL_IS_SCROLLED_WINDOW (self)); + g_assert (min_height != NULL); + g_assert (nat_height != NULL); + + min_content_height = gtk_scrolled_window_get_min_content_height (GTK_SCROLLED_WINDOW (self)); + max_content_height = gtk_scrolled_window_get_max_content_height (GTK_SCROLLED_WINDOW (self)); + border_width = gtk_container_get_border_width (GTK_CONTAINER (self)); + child = gtk_bin_get_child (GTK_BIN (self)); + + if (child == NULL) + { + *min_height = 0; + *nat_height = 0; + return; + } + + gtk_widget_get_preferred_height_for_width (child, width, min_height, nat_height); + + if (min_content_height > 0) + *min_height = MAX (*min_height, min_content_height); + else + *min_height = 1; + + if (max_content_height > 0) + *nat_height = MIN (*nat_height, max_content_height); + + *nat_height = MAX (*min_height, *nat_height); + + /* + * Special case for our use. What we should probably do is have a "grow with child + * range" but still fill into larger space with vexpand. + * + * This tries to enfoce at least a 5x3 ratio for the content, for asthetic reasons. + */ + if (*nat_height > width && *min_height < (width / 5 * 3)) + *min_height = (width / 5 * 3); + + *min_height += border_width * 2; + *nat_height += border_width * 2; +} + +static GtkSizeRequestMode +dzl_scrolled_window_get_request_mode (GtkWidget *widget) +{ + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + +static void +dzl_scrolled_window_class_init (DzlScrolledWindowClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + widget_class->get_preferred_height_for_width = dzl_scrolled_window_get_preferred_height_for_width; + widget_class->get_request_mode = dzl_scrolled_window_get_request_mode; +} + +static void +dzl_scrolled_window_init (DzlScrolledWindow *self) +{ +} diff --git a/src/widgets/dzl-scrolled-window.h b/src/widgets/dzl-scrolled-window.h new file mode 100644 index 0000000..370cdf1 --- /dev/null +++ b/src/widgets/dzl-scrolled-window.h @@ -0,0 +1,35 @@ +/* dzl-scrolled-window.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_SCROLLED_WINDOW_H +#define DZL_SCROLLED_WINDOW_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SCROLLED_WINDOW (dzl_scrolled_window_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlScrolledWindow, dzl_scrolled_window, DZL, SCROLLED_WINDOW, GtkScrolledWindow) + +G_END_DECLS + +#endif /* DZL_SCROLLED_WINDOW_H */ diff --git a/src/widgets/dzl-search-bar.c b/src/widgets/dzl-search-bar.c new file mode 100644 index 0000000..4897a78 --- /dev/null +++ b/src/widgets/dzl-search-bar.c @@ -0,0 +1,481 @@ +/* dzl-search-bar.c + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#define G_LOG_DOMAIN "dzl-search-bar" + +#include "config.h" + +#include + +#include "bindings/dzl-signal-group.h" +#include "widgets/dzl-search-bar.h" + +typedef struct +{ + GtkRevealer *revealer; + GtkBox *box; + GtkSearchEntry *entry; + GtkButton *close_button; + + DzlSignalGroup *window_signals; + + guint search_mode_enabled : 1; +} DzlSearchBarPrivate; + +static void dzl_search_bar_init_buildable (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (DzlSearchBar, dzl_search_bar, GTK_TYPE_BIN, + G_ADD_PRIVATE (DzlSearchBar) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, + dzl_search_bar_init_buildable)) + +enum { + PROP_0, + PROP_SHOW_CLOSE_BUTTON, + PROP_SEARCH_MODE_ENABLED, + LAST_PROP +}; + +enum { + ACTIVATE, + REVEAL, + LAST_SIGNAL +}; + +static GParamSpec *properties [LAST_PROP]; +static guint signals [LAST_SIGNAL]; + +static void +dzl_search_bar__entry_activate (DzlSearchBar *self, + GtkSearchEntry *entry) +{ + g_assert (DZL_IS_SEARCH_BAR (self)); + g_assert (GTK_IS_SEARCH_ENTRY (entry)); + + g_signal_emit (self, signals [ACTIVATE], 0); +} + +static gboolean +is_modifier_key (const GdkEventKey *event) +{ + static const guint modifier_keyvals[] = { + GDK_KEY_Shift_L, GDK_KEY_Shift_R, GDK_KEY_Shift_Lock, + GDK_KEY_Caps_Lock, GDK_KEY_ISO_Lock, GDK_KEY_Control_L, + GDK_KEY_Control_R, GDK_KEY_Meta_L, GDK_KEY_Meta_R, + GDK_KEY_Alt_L, GDK_KEY_Alt_R, GDK_KEY_Super_L, GDK_KEY_Super_R, + GDK_KEY_Hyper_L, GDK_KEY_Hyper_R, GDK_KEY_ISO_Level3_Shift, + GDK_KEY_ISO_Next_Group, GDK_KEY_ISO_Prev_Group, + GDK_KEY_ISO_First_Group, GDK_KEY_ISO_Last_Group, + GDK_KEY_Mode_switch, GDK_KEY_Num_Lock, GDK_KEY_Multi_key, + GDK_KEY_Scroll_Lock, + 0 + }; + const guint *ac_val; + + g_return_val_if_fail (event != NULL, FALSE); + + ac_val = modifier_keyvals; + + while (*ac_val) + { + if (event->keyval == *ac_val++) + return TRUE; + } + + return FALSE; +} + +static gboolean +toplevel_key_press_event_before (DzlSearchBar *self, + GdkEventKey *event, + GtkWindow *toplevel) +{ + DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self); + + g_assert (DZL_IS_SEARCH_BAR (self)); + g_assert (event != NULL); + g_assert (GTK_IS_WINDOW (toplevel)); + + switch (event->keyval) + { + case GDK_KEY_Escape: + if (priv->search_mode_enabled && gtk_widget_has_focus (GTK_WIDGET (priv->entry))) + { + dzl_search_bar_set_search_mode_enabled (self, FALSE); + return GDK_EVENT_STOP; + } + break; + + default: + break; + } + + return GDK_EVENT_PROPAGATE; +} + +static gboolean +toplevel_key_press_event_after (DzlSearchBar *self, + GdkEventKey *event, + GtkWindow *toplevel) +{ + DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self); + GtkWidget *entry; + + g_assert (DZL_IS_SEARCH_BAR (self)); + g_assert (event != NULL); + g_assert (GTK_IS_WINDOW (toplevel)); + + entry = GTK_WIDGET (priv->entry); + + switch (event->keyval) + { + case GDK_KEY_Escape: + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + case GDK_KEY_End: + case GDK_KEY_KP_End: + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: + case GDK_KEY_KP_Tab: + case GDK_KEY_Tab: + /* ignore keynav */ + break; + + default: + if (((event->state & GDK_MOD1_MASK) != 0) || + ((event->state & GDK_CONTROL_MASK) != 0) || + priv->search_mode_enabled || + is_modifier_key (event)) + break; + + dzl_search_bar_set_search_mode_enabled (self, TRUE); + + return GTK_WIDGET_GET_CLASS (entry)->key_press_event (entry, event); + } + + return GDK_EVENT_PROPAGATE; +} + +static void +dzl_search_bar_hierarchy_changed (GtkWidget *widget, + GtkWidget *old_toplevel) +{ + DzlSearchBar *self = (DzlSearchBar *)widget; + DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self); + GtkWidget *toplevel; + + g_assert (DZL_IS_SEARCH_BAR (self)); + + toplevel = gtk_widget_get_toplevel (widget); + + if (GTK_IS_WINDOW (toplevel)) + dzl_signal_group_set_target (priv->window_signals, toplevel); + else + dzl_signal_group_set_target (priv->window_signals, NULL); +} + +static void +dzl_search_bar_reveal (DzlSearchBar *self) +{ + g_assert (DZL_IS_SEARCH_BAR (self)); + + dzl_search_bar_set_search_mode_enabled (self, TRUE); +} + +static GObject * +dzl_search_bar_get_internal_child (GtkBuildable *buildable, + GtkBuilder *builder, + const gchar *childname) +{ + DzlSearchBar *self = (DzlSearchBar *)buildable; + DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self); + + g_assert (GTK_IS_BUILDABLE (buildable)); + g_assert (DZL_IS_SEARCH_BAR (self)); + g_assert (GTK_IS_BUILDER (builder)); + g_assert (childname != NULL); + + if (g_strcmp0 (childname, "entry") == 0) + return G_OBJECT (priv->entry); + else if (g_strcmp0 (childname, "revealer") == 0) + return G_OBJECT (priv->revealer); + + return NULL; +} + +static void +dzl_search_bar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSearchBar *self = DZL_SEARCH_BAR (object); + + switch (prop_id) + { + case PROP_SEARCH_MODE_ENABLED: + g_value_set_boolean (value, dzl_search_bar_get_search_mode_enabled (self)); + break; + + case PROP_SHOW_CLOSE_BUTTON: + g_value_set_boolean (value, dzl_search_bar_get_show_close_button (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_search_bar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSearchBar *self = DZL_SEARCH_BAR (object); + + switch (prop_id) + { + case PROP_SEARCH_MODE_ENABLED: + dzl_search_bar_set_search_mode_enabled (self, g_value_get_boolean (value)); + break; + + case PROP_SHOW_CLOSE_BUTTON: + dzl_search_bar_set_show_close_button (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_search_bar_finalize (GObject *object) +{ + DzlSearchBar *self = (DzlSearchBar *)object; + DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self); + + g_clear_object (&priv->window_signals); + + G_OBJECT_CLASS (dzl_search_bar_parent_class)->finalize (object); +} + +static void +dzl_search_bar_class_init (DzlSearchBarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_search_bar_finalize; + object_class->get_property = dzl_search_bar_get_property; + object_class->set_property = dzl_search_bar_set_property; + + widget_class->hierarchy_changed = dzl_search_bar_hierarchy_changed; + + properties [PROP_SEARCH_MODE_ENABLED] = + g_param_spec_boolean ("search-mode-enabled", + "Search Mode Enabled", + "Search Mode Enabled", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_SHOW_CLOSE_BUTTON] = + g_param_spec_boolean ("show-close-button", + "Show Close Button", + "Show Close Button", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + signals [ACTIVATE] = + g_signal_new ("activate", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + 0, NULL, NULL, NULL, G_TYPE_NONE, 0); + + signals [REVEAL] = + g_signal_new_class_handler ("reveal", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_CALLBACK (dzl_search_bar_reveal), + NULL, NULL, NULL, G_TYPE_NONE, 0); + + gtk_widget_class_set_css_name (widget_class, "dzlsearchbar"); +} + +static void +dzl_search_bar_init_buildable (GtkBuildableIface *iface) +{ + iface->get_internal_child = dzl_search_bar_get_internal_child; +} + +static void +dzl_search_bar_init (DzlSearchBar *self) +{ + DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self); + GtkStyleContext *style_context; + GtkBox *box; + + priv->window_signals = dzl_signal_group_new (GTK_TYPE_WINDOW); + dzl_signal_group_connect_object (priv->window_signals, + "key-press-event", + G_CALLBACK (toplevel_key_press_event_before), + self, + G_CONNECT_SWAPPED); + dzl_signal_group_connect_object (priv->window_signals, + "key-press-event", + G_CALLBACK (toplevel_key_press_event_after), + self, + G_CONNECT_SWAPPED | G_CONNECT_AFTER); + + priv->revealer = + g_object_new (GTK_TYPE_REVEALER, + "transition-type", GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN, + "visible", TRUE, + NULL); + /* outer box used for styling */ + box = + g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "visible", TRUE, + NULL); + priv->box = + g_object_new (GTK_TYPE_BOX, + "hexpand", TRUE, + "margin-bottom", 3, + "margin-end", 6, + "margin-start", 6, + "margin-top", 3, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "visible", TRUE, + NULL); + priv->entry = + g_object_connect (g_object_new (GTK_TYPE_SEARCH_ENTRY, + "placeholder-text", _("Search"), + "visible", TRUE, + NULL), + "swapped_object_signal::activate", dzl_search_bar__entry_activate, self, + NULL); + priv->close_button = + g_object_new (GTK_TYPE_BUTTON, + "child", g_object_new (GTK_TYPE_IMAGE, + "icon-name", "window-close-symbolic", + "visible", TRUE, + NULL), + "visible", FALSE, + NULL); + + style_context = gtk_widget_get_style_context (GTK_WIDGET (box)); + gtk_style_context_add_class (style_context, "search-bar"); + + gtk_container_add (GTK_CONTAINER (priv->revealer), GTK_WIDGET (box)); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->box)); + gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (priv->revealer)); + gtk_container_add_with_properties (GTK_CONTAINER (priv->box), + GTK_WIDGET (priv->close_button), + "fill", FALSE, + "pack-type", GTK_PACK_END, + NULL); + gtk_box_set_center_widget (priv->box, GTK_WIDGET (priv->entry)); +} + +GtkWidget * +dzl_search_bar_new (void) +{ + return g_object_new (DZL_TYPE_SEARCH_BAR, NULL); +} + +gboolean +dzl_search_bar_get_search_mode_enabled (DzlSearchBar *self) +{ + DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SEARCH_BAR (self), FALSE); + + return priv->search_mode_enabled; +} + +void +dzl_search_bar_set_search_mode_enabled (DzlSearchBar *self, + gboolean search_mode_enabled) +{ + DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self); + + g_return_if_fail (DZL_IS_SEARCH_BAR (self)); + + search_mode_enabled = !!search_mode_enabled; + + if (search_mode_enabled != priv->search_mode_enabled) + { + priv->search_mode_enabled = search_mode_enabled; + gtk_revealer_set_reveal_child (priv->revealer, search_mode_enabled); + gtk_entry_set_text (GTK_ENTRY (priv->entry), ""); + if (search_mode_enabled) + gtk_widget_grab_focus (GTK_WIDGET (priv->entry)); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SEARCH_MODE_ENABLED]); + } +} + +gboolean +dzl_search_bar_get_show_close_button (DzlSearchBar *self) +{ + DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SEARCH_BAR (self), FALSE); + + return gtk_widget_get_visible (GTK_WIDGET (priv->close_button)); +} + +void +dzl_search_bar_set_show_close_button (DzlSearchBar *self, + gboolean show_close_button) +{ + DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self); + + g_return_if_fail (DZL_IS_SEARCH_BAR (self)); + + gtk_widget_set_visible (GTK_WIDGET (priv->close_button), show_close_button); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_CLOSE_BUTTON]); +} + +/** + * dzl_search_bar_get_entry: + * + * Returns: (transfer none) (type Gtk.SearchEntry): A #GtkSearchEntry. + */ +GtkWidget * +dzl_search_bar_get_entry (DzlSearchBar *self) +{ + DzlSearchBarPrivate *priv = dzl_search_bar_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SEARCH_BAR (self), NULL); + + return GTK_WIDGET (priv->entry); +} diff --git a/src/widgets/dzl-search-bar.h b/src/widgets/dzl-search-bar.h new file mode 100644 index 0000000..21229e7 --- /dev/null +++ b/src/widgets/dzl-search-bar.h @@ -0,0 +1,55 @@ +/* dzl-search-bar.h + * + * Copyright (C) 2015 Christian Hergert + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#ifndef DZL_SEARCH_BAR_H +#define DZL_SEARCH_BAR_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SEARCH_BAR (dzl_search_bar_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlSearchBar, dzl_search_bar, DZL, SEARCH_BAR, GtkBin) + +struct _DzlSearchBarClass +{ + GtkBinClass parent_class; +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_search_bar_new (void); +DZL_AVAILABLE_IN_ALL +gboolean dzl_search_bar_get_search_mode_enabled (DzlSearchBar *self); +DZL_AVAILABLE_IN_ALL +void dzl_search_bar_set_search_mode_enabled (DzlSearchBar *self, + gboolean search_mode_enabled); +DZL_AVAILABLE_IN_ALL +gboolean dzl_search_bar_get_show_close_button (DzlSearchBar *self); +DZL_AVAILABLE_IN_ALL +void dzl_search_bar_set_show_close_button (DzlSearchBar *self, + gboolean show_close_button); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_search_bar_get_entry (DzlSearchBar *self); + +G_END_DECLS + +#endif /* DZL_SEARCH_BAR_H */ diff --git a/src/widgets/dzl-simple-label.c b/src/widgets/dzl-simple-label.c new file mode 100644 index 0000000..0b154de --- /dev/null +++ b/src/widgets/dzl-simple-label.c @@ -0,0 +1,392 @@ +/* dzl-simple-label.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#include + +#include "config.h" + +#include "dzl-simple-label.h" + +struct _DzlSimpleLabel +{ + GtkWidget parent_instance; + + gchar *label; + gint label_len; + + gint width_chars; + + PangoLayout *cached_layout; + + gfloat xalign; + + gint cached_width_request; + gint cached_height_request; + gint real_width; + gint real_height; +}; + +G_DEFINE_TYPE (DzlSimpleLabel, dzl_simple_label, GTK_TYPE_WIDGET) + +enum { + PROP_0, + PROP_LABEL, + PROP_WIDTH_CHARS, + PROP_XALIGN, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static void +dzl_simple_label_calculate_size (DzlSimpleLabel *self) +{ + PangoContext *context; + PangoLayout *layout; + + g_assert (DZL_IS_SIMPLE_LABEL (self)); + + self->cached_height_request = -1; + self->cached_width_request = -1; + + if (self->label == NULL && self->width_chars <= 0) + { + self->cached_height_request = 0; + self->cached_width_request = 0; + self->real_width = 0; + self->real_height = 0; + return; + } + + if (NULL == (context = gtk_widget_get_pango_context (GTK_WIDGET (self)))) + return; + + g_clear_object (&self->cached_layout); + + layout = pango_layout_new (context); + + if (self->width_chars >= 0) + { + gchar str[self->width_chars]; + memset (str, '9', self->width_chars); + pango_layout_set_text (layout, str, self->width_chars); + } + else + { + pango_layout_set_text (layout, self->label, self->label_len); + } + + pango_layout_get_pixel_size (layout, + &self->cached_width_request, + &self->cached_height_request); + + if (self->label != NULL) + pango_layout_set_text (layout, self->label, self->label_len); + else + pango_layout_set_text (layout, "", 0); + + pango_layout_get_pixel_size (layout, &self->real_width, &self->real_height); + + if (self->real_width > self->cached_width_request) + self->cached_width_request = self->real_width; + + if (self->real_height > self->cached_height_request) + self->cached_height_request = self->real_height; + + self->cached_layout = layout; +} + +static void +dzl_simple_label_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + DzlSimpleLabel *self = (DzlSimpleLabel *)widget; + + g_assert (DZL_IS_SIMPLE_LABEL (self)); + + if (self->cached_width_request == -1) + dzl_simple_label_calculate_size (self); + + *min_width = *nat_width = self->cached_width_request; +} + +static void +dzl_simple_label_get_preferred_height (GtkWidget *widget, + gint *min_height, + gint *nat_height) +{ + DzlSimpleLabel *self = (DzlSimpleLabel *)widget; + + g_assert (DZL_IS_SIMPLE_LABEL (self)); + + if (self->cached_height_request == -1) + dzl_simple_label_calculate_size (self); + + *min_height = *nat_height = self->cached_height_request; +} + +static gboolean +dzl_simple_label_draw (GtkWidget *widget, + cairo_t *cr) +{ + DzlSimpleLabel *self = (DzlSimpleLabel *)widget; + GtkAllocation alloc; + gdouble x; + gdouble y; + + if (self->label == NULL) + return GDK_EVENT_PROPAGATE; + + gtk_widget_get_allocation (widget, &alloc); + + if (self->cached_width_request == -1 || + self->cached_height_request == -1 || + self->cached_layout == NULL) + dzl_simple_label_calculate_size (self); + + x = (alloc.width - self->real_width) * self->xalign; + y = (alloc.height - self->real_height) / 2; + + /* + * We should support baseline here, but we don't actually + * get a real baseline yet where this is used in Builder, + * so I'm going to punt on it. + */ + + gtk_render_layout (gtk_widget_get_style_context (widget), + cr, x, y, self->cached_layout); + + return GDK_EVENT_PROPAGATE; +} + +static void +dzl_simple_label_destroy (GtkWidget *widget) +{ + DzlSimpleLabel *self = (DzlSimpleLabel *)widget; + + g_clear_pointer (&self->label, g_free); + g_clear_object (&self->cached_layout); + + GTK_WIDGET_CLASS (dzl_simple_label_parent_class)->destroy (widget); +} + +static void +dzl_simple_label_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSimpleLabel *self = DZL_SIMPLE_LABEL (object); + + switch (prop_id) + { + case PROP_LABEL: + g_value_set_string (value, self->label); + break; + + case PROP_WIDTH_CHARS: + g_value_set_int (value, self->width_chars); + break; + + case PROP_XALIGN: + g_value_set_float (value, self->xalign); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_simple_label_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSimpleLabel *self = DZL_SIMPLE_LABEL (object); + + switch (prop_id) + { + case PROP_LABEL: + dzl_simple_label_set_label (self, g_value_get_string (value)); + break; + + case PROP_WIDTH_CHARS: + dzl_simple_label_set_width_chars (self, g_value_get_int (value)); + break; + + case PROP_XALIGN: + dzl_simple_label_set_xalign (self, g_value_get_float (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_simple_label_class_init (DzlSimpleLabelClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = dzl_simple_label_get_property; + object_class->set_property = dzl_simple_label_set_property; + + widget_class->destroy = dzl_simple_label_destroy; + widget_class->draw = dzl_simple_label_draw; + widget_class->get_preferred_width = dzl_simple_label_get_preferred_width; + widget_class->get_preferred_height = dzl_simple_label_get_preferred_height; + + gtk_widget_class_set_css_name (widget_class, "label"); + + properties [PROP_LABEL] = + g_param_spec_string ("label", + NULL, + NULL, + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_WIDTH_CHARS] = + g_param_spec_int ("width-chars", + NULL, + NULL, + -1, + 1000, + -1, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_XALIGN] = + g_param_spec_float ("xalign", + NULL, + NULL, + 0.0, + 1.0, + 0.5, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +dzl_simple_label_init (DzlSimpleLabel *self) +{ + gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); + + self->width_chars = -1; + self->xalign = 0.5; +} + +GtkWidget * +dzl_simple_label_new (const gchar *label) +{ + return g_object_new (DZL_TYPE_SIMPLE_LABEL, NULL); +} + +const gchar * +dzl_simple_label_get_label (DzlSimpleLabel *self) +{ + g_return_val_if_fail (DZL_IS_SIMPLE_LABEL (self), NULL); + + return self->label; +} + +void +dzl_simple_label_set_label (DzlSimpleLabel *self, + const gchar *label) +{ + g_return_if_fail (DZL_IS_SIMPLE_LABEL (self)); + + if (g_strcmp0 (label, self->label) != 0) + { + gint last_len = self->label_len; + + g_free (self->label); + + self->label = g_strdup (label); + self->label_len = label ? strlen (label) : 0; + + self->cached_width_request = -1; + self->cached_height_request = -1; + + /* + * If width chars is not set, then we always have to calculate the size + * change. If we are growing larger, we also might have to relcalculate + * if the new length is larger than our precalculated length. If we are + * shrinking from an overgrow position, we also have to resize. + * + * But in *most* cases, we can avoid the resize altogether. This is + * a necessity in the situations where this widget is valuable (such + * as the cursor coordinate label in Builder). + */ + if ((self->width_chars < 0) || + ((self->label_len > self->width_chars) && (last_len != self->label_len)) || + ((last_len > self->width_chars) && (self->label_len <= self->width_chars))) + gtk_widget_queue_resize (GTK_WIDGET (self)); + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LABEL]); + } +} + +gint +dzl_simple_label_get_width_chars (DzlSimpleLabel *self) +{ + g_return_val_if_fail (DZL_IS_SIMPLE_LABEL (self), -1); + + return self->width_chars; +} + +void +dzl_simple_label_set_width_chars (DzlSimpleLabel *self, + gint width_chars) +{ + g_return_if_fail (DZL_IS_SIMPLE_LABEL (self)); + g_return_if_fail (width_chars >= -1); + g_return_if_fail (width_chars <= 100); + + if (self->width_chars != width_chars) + { + self->width_chars = width_chars; + self->cached_width_request = -1; + self->cached_height_request = -1; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WIDTH_CHARS]); + } +} + +gfloat +dzl_simple_label_get_xalign (DzlSimpleLabel *self) +{ + g_return_val_if_fail (DZL_IS_SIMPLE_LABEL (self), 0.0); + + return self->xalign; +} + +void +dzl_simple_label_set_xalign (DzlSimpleLabel *self, + gfloat xalign) +{ + if (self->xalign != xalign) + { + self->xalign = xalign; + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_XALIGN]); + } +} diff --git a/src/widgets/dzl-simple-label.h b/src/widgets/dzl-simple-label.h new file mode 100644 index 0000000..1df482f --- /dev/null +++ b/src/widgets/dzl-simple-label.h @@ -0,0 +1,60 @@ +/* dzl-simple-label.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_SIMPLE_LABEL_H +#define DZL_SIMPLE_LABEL_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +/* + * This widget has one very simple purpose. Allow updating a simple + * amount of text without causing resizes to propagate up the widget + * hierarchy. Therefore, it only supports a very minimal amount of + * features. The label text, and the width-chars to use for sizing. + */ + +#define DZL_TYPE_SIMPLE_LABEL (dzl_simple_label_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (DzlSimpleLabel, dzl_simple_label, DZL, SIMPLE_LABEL, GtkWidget) + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_simple_label_new (const gchar *label); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_simple_label_get_label (DzlSimpleLabel *self); +DZL_AVAILABLE_IN_ALL +void dzl_simple_label_set_label (DzlSimpleLabel *self, + const gchar *label); +DZL_AVAILABLE_IN_ALL +gint dzl_simple_label_get_width_chars (DzlSimpleLabel *self); +DZL_AVAILABLE_IN_ALL +void dzl_simple_label_set_width_chars (DzlSimpleLabel *self, + gint width_chars); +DZL_AVAILABLE_IN_ALL +gfloat dzl_simple_label_get_xalign (DzlSimpleLabel *self); +DZL_AVAILABLE_IN_ALL +void dzl_simple_label_set_xalign (DzlSimpleLabel *self, + gfloat xalign); + +G_END_DECLS + +#endif /* DZL_SIMPLE_LABEL_H */ diff --git a/src/widgets/dzl-simple-popover.c b/src/widgets/dzl-simple-popover.c new file mode 100644 index 0000000..e69e06f --- /dev/null +++ b/src/widgets/dzl-simple-popover.c @@ -0,0 +1,417 @@ +/* dzl-simple-popover.c + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#include "config.h" + +#include "dzl-simple-popover.h" + +typedef struct +{ + GtkPopover parent_instance; + + GtkLabel *title; + GtkLabel *message; + GtkEntry *entry; + GtkButton *button; +} DzlSimplePopoverPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlSimplePopover, dzl_simple_popover, GTK_TYPE_POPOVER) + +enum { + PROP_0, + PROP_BUTTON_TEXT, + PROP_MESSAGE, + PROP_READY, + PROP_TEXT, + PROP_TITLE, + LAST_PROP +}; + +enum { + ACTIVATE, + CHANGED, + INSERT_TEXT, + LAST_SIGNAL +}; + +static GParamSpec *properties [LAST_PROP]; +static guint signals [LAST_SIGNAL]; + +const gchar * +dzl_simple_popover_get_button_text (DzlSimplePopover *self) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SIMPLE_POPOVER (self), NULL); + + return gtk_button_get_label (priv->button); +} + +void +dzl_simple_popover_set_button_text (DzlSimplePopover *self, + const gchar *button_text) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + + g_return_if_fail (DZL_IS_SIMPLE_POPOVER (self)); + + gtk_button_set_label (priv->button, button_text); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUTTON_TEXT]); +} + +const gchar * +dzl_simple_popover_get_message (DzlSimplePopover *self) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SIMPLE_POPOVER (self), NULL); + + return gtk_label_get_text (priv->message); +} + +void +dzl_simple_popover_set_message (DzlSimplePopover *self, + const gchar *message) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + + g_return_if_fail (DZL_IS_SIMPLE_POPOVER (self)); + + gtk_label_set_label (priv->message, message); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]); +} + +gboolean +dzl_simple_popover_get_ready (DzlSimplePopover *self) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SIMPLE_POPOVER (self), FALSE); + + return gtk_widget_get_sensitive (GTK_WIDGET (priv->button)); +} + +void +dzl_simple_popover_set_ready (DzlSimplePopover *self, + gboolean ready) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + + g_return_if_fail (DZL_IS_SIMPLE_POPOVER (self)); + + gtk_widget_set_sensitive (GTK_WIDGET (priv->button), ready); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_READY]); +} + +const gchar * +dzl_simple_popover_get_text (DzlSimplePopover *self) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SIMPLE_POPOVER (self), NULL); + + return gtk_entry_get_text (priv->entry); +} + +void +dzl_simple_popover_set_text (DzlSimplePopover *self, + const gchar *text) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + + g_return_if_fail (DZL_IS_SIMPLE_POPOVER (self)); + + gtk_entry_set_text (priv->entry, text); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TEXT]); +} + +const gchar * +dzl_simple_popover_get_title (DzlSimplePopover *self) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SIMPLE_POPOVER (self), NULL); + + return gtk_label_get_label (priv->title); +} + +void +dzl_simple_popover_set_title (DzlSimplePopover *self, + const gchar *title) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + + g_return_if_fail (DZL_IS_SIMPLE_POPOVER (self)); + + gtk_label_set_label (priv->title, title); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]); +} + +static void +dzl_simple_popover_button_clicked (DzlSimplePopover *self, + GtkButton *button) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + const gchar *text; + + g_assert (DZL_IS_SIMPLE_POPOVER (self)); + g_assert (GTK_IS_BUTTON (button)); + + text = gtk_entry_get_text (GTK_ENTRY (priv->entry)); + g_signal_emit (self, signals [ACTIVATE], 0, text); + gtk_popover_popdown (GTK_POPOVER (self)); +} + +static void +dzl_simple_popover_entry_activate (DzlSimplePopover *self, + GtkEntry *entry) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + + g_assert (DZL_IS_SIMPLE_POPOVER (self)); + g_assert (GTK_IS_ENTRY (entry)); + + if (dzl_simple_popover_get_ready (self)) + gtk_widget_activate (GTK_WIDGET (priv->button)); +} + +static void +dzl_simple_popover_entry_changed (DzlSimplePopover *self, + GtkEntry *entry) +{ + g_assert (DZL_IS_SIMPLE_POPOVER (self)); + g_assert (GTK_IS_ENTRY (entry)); + + g_signal_emit (self, signals [CHANGED], 0); +} + +static void +dzl_simple_popover_entry_insert_text (DzlSimplePopover *self, + gchar *new_text, + gint new_text_length, + gint *position, + GtkEntry *entry) +{ + gboolean ret = GDK_EVENT_PROPAGATE; + guint pos; + guint n_chars; + + g_assert (DZL_IS_SIMPLE_POPOVER (self)); + g_assert (new_text != NULL); + g_assert (position != NULL); + + pos = *position; + n_chars = (new_text_length >= 0) ? new_text_length : g_utf8_strlen (new_text, -1); + + g_signal_emit (self, signals [INSERT_TEXT], 0, pos, new_text, n_chars, &ret); + + if (ret == GDK_EVENT_STOP) + g_signal_stop_emission_by_name (entry, "insert-text"); +} + +static void +dzl_simple_popover_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSimplePopover *self = DZL_SIMPLE_POPOVER (object); + + switch (prop_id) + { + case PROP_BUTTON_TEXT: + g_value_set_string (value, dzl_simple_popover_get_button_text (self)); + break; + + case PROP_MESSAGE: + g_value_set_string (value, dzl_simple_popover_get_message (self)); + break; + + case PROP_READY: + g_value_set_boolean (value, dzl_simple_popover_get_ready (self)); + break; + + case PROP_TEXT: + g_value_set_string (value, dzl_simple_popover_get_text (self)); + break; + + case PROP_TITLE: + g_value_set_string (value, dzl_simple_popover_get_title (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_simple_popover_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSimplePopover *self = DZL_SIMPLE_POPOVER (object); + + switch (prop_id) + { + case PROP_BUTTON_TEXT: + dzl_simple_popover_set_button_text (self, g_value_get_string (value)); + break; + + case PROP_MESSAGE: + dzl_simple_popover_set_message (self, g_value_get_string (value)); + break; + + case PROP_READY: + dzl_simple_popover_set_ready (self, g_value_get_boolean (value)); + break; + + case PROP_TEXT: + dzl_simple_popover_set_text (self, g_value_get_string (value)); + break; + + case PROP_TITLE: + dzl_simple_popover_set_title (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_simple_popover_class_init (DzlSimplePopoverClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = dzl_simple_popover_get_property; + object_class->set_property = dzl_simple_popover_set_property; + + properties [PROP_BUTTON_TEXT] = + g_param_spec_string ("button-text", + "Button Text", + "Button Text", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_MESSAGE] = + g_param_spec_string ("message", + "Message", + "Message", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_READY] = + g_param_spec_boolean ("ready", + "Ready", + "Ready", + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TEXT] = + g_param_spec_string ("text", + "Text", + "Text", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "Title", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + signals [ACTIVATE] = + g_signal_new ("activate", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlSimplePopoverClass, activate), + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_STRING); + + signals [CHANGED] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlSimplePopoverClass, insert_text), + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + signals [INSERT_TEXT] = + g_signal_new ("insert-text", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlSimplePopoverClass, insert_text), + NULL, NULL, NULL, + G_TYPE_BOOLEAN, + 3, + G_TYPE_UINT, + G_TYPE_STRING, + G_TYPE_UINT); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-simple-popover.ui"); + gtk_widget_class_bind_template_child_private (widget_class, DzlSimplePopover, title); + gtk_widget_class_bind_template_child_private (widget_class, DzlSimplePopover, message); + gtk_widget_class_bind_template_child_private (widget_class, DzlSimplePopover, entry); + gtk_widget_class_bind_template_child_private (widget_class, DzlSimplePopover, button); +} + +static void +dzl_simple_popover_init (DzlSimplePopover *self) +{ + DzlSimplePopoverPrivate *priv = dzl_simple_popover_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); + + g_signal_connect_object (priv->button, + "clicked", + G_CALLBACK (dzl_simple_popover_button_clicked), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (priv->entry, + "changed", + G_CALLBACK (dzl_simple_popover_entry_changed), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (priv->entry, + "activate", + G_CALLBACK (dzl_simple_popover_entry_activate), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (priv->entry, + "insert-text", + G_CALLBACK (dzl_simple_popover_entry_insert_text), + self, + G_CONNECT_SWAPPED); +} + +GtkWidget * +dzl_simple_popover_new (void) +{ + return g_object_new (DZL_TYPE_SIMPLE_POPOVER, NULL); +} diff --git a/src/widgets/dzl-simple-popover.h b/src/widgets/dzl-simple-popover.h new file mode 100644 index 0000000..1dcb2dd --- /dev/null +++ b/src/widgets/dzl-simple-popover.h @@ -0,0 +1,104 @@ +/* dzl-simple-popover.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_SIMPLE_POPOVER_H +#define DZL_SIMPLE_POPOVER_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SIMPLE_POPOVER (dzl_simple_popover_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlSimplePopover, dzl_simple_popover, DZL, SIMPLE_POPOVER, GtkPopover) + +struct _DzlSimplePopoverClass +{ + GtkPopoverClass parent; + + /** + * DzlSimplePopover::activate: + * @self: The #DzlSimplePopover instance. + * @text: The text at the time of activation. + * + * This signal is emitted when the popover's forward button is activated. + * Connect to this signal to perform your forward progress. + */ + void (*activate) (DzlSimplePopover *self, + const gchar *text); + + /** + * DzlSimplePopover::insert-text: + * @self: A #DzlSimplePopover. + * @position: the position in UTF-8 characters. + * @chars: the NULL terminated UTF-8 text to insert. + * @n_chars: the number of UTF-8 characters in chars. + * + * Use this signal to determine if text should be allowed to be inserted + * into the text buffer. Return GDK_EVENT_STOP to prevent the text from + * being inserted. + */ + gboolean (*insert_text) (DzlSimplePopover *self, + guint position, + const gchar *chars, + guint n_chars); + + + /** + * DzlSimplePopover::changed: + * @self: A #DzlSimplePopover. + * + * This signal is emitted when the entry text changes. + */ + void (*changed) (DzlSimplePopover *self); +}; + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_simple_popover_new (void); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_simple_popover_get_text (DzlSimplePopover *self); +DZL_AVAILABLE_IN_ALL +void dzl_simple_popover_set_text (DzlSimplePopover *self, + const gchar *text); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_simple_popover_get_message (DzlSimplePopover *self); +DZL_AVAILABLE_IN_ALL +void dzl_simple_popover_set_message (DzlSimplePopover *self, + const gchar *message); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_simple_popover_get_title (DzlSimplePopover *self); +DZL_AVAILABLE_IN_ALL +void dzl_simple_popover_set_title (DzlSimplePopover *self, + const gchar *title); +DZL_AVAILABLE_IN_ALL +const gchar *dzl_simple_popover_get_button_text (DzlSimplePopover *self); +DZL_AVAILABLE_IN_ALL +void dzl_simple_popover_set_button_text (DzlSimplePopover *self, + const gchar *button_text); +DZL_AVAILABLE_IN_ALL +gboolean dzl_simple_popover_get_ready (DzlSimplePopover *self); +DZL_AVAILABLE_IN_ALL +void dzl_simple_popover_set_ready (DzlSimplePopover *self, + gboolean ready); + +G_END_DECLS + +#endif /* DZL_SIMPLE_POPOVER_H */ diff --git a/src/widgets/dzl-simple-popover.ui b/src/widgets/dzl-simple-popover.ui new file mode 100644 index 0000000..02b58b0 --- /dev/null +++ b/src/widgets/dzl-simple-popover.ui @@ -0,0 +1,55 @@ + + + + + diff --git a/src/widgets/dzl-slider.c b/src/widgets/dzl-slider.c new file mode 100644 index 0000000..55f3d10 --- /dev/null +++ b/src/widgets/dzl-slider.c @@ -0,0 +1,975 @@ +/* dzl-slider.c + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-slider" + +#include "config.h" + +#include + +#include "animation/dzl-animation.h" +#include "widgets/dzl-slider.h" +#include "util/dzl-util-private.h" + +typedef struct +{ + GtkWidget *widget; + GdkWindow *window; + GtkAllocation allocation; + DzlSliderPosition position : 3; +} DzlSliderChild; + +typedef struct +{ + GtkAdjustment *h_adj; + GtkAdjustment *v_adj; + + DzlAnimation *h_anim; + DzlAnimation *v_anim; + + GPtrArray *children; + + DzlSliderPosition position : 3; +} DzlSliderPrivate; + +static void buildable_iface_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (DzlSlider, dzl_slider, GTK_TYPE_CONTAINER, + G_ADD_PRIVATE (DzlSlider) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init)) + +enum { + PROP_0, + PROP_POSITION, + LAST_PROP +}; + +enum { + CHILD_PROP_0, + CHILD_PROP_POSITION, +}; + +#define ANIMATION_MODE DZL_ANIMATION_EASE_IN_QUAD +#define ANIMATION_DURATION 150 + +static GParamSpec *properties [LAST_PROP]; + +static void +dzl_slider_child_free (DzlSliderChild *child) +{ + g_slice_free (DzlSliderChild, child); +} + +static void +dzl_slider_compute_margin (DzlSlider *self, + gint *x_margin, + gint *y_margin) +{ + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + gdouble x_ratio; + gdouble y_ratio; + gsize i; + gint real_top_margin = 0; + gint real_bottom_margin = 0; + gint real_left_margin = 0; + gint real_right_margin = 0; + + g_assert (DZL_IS_SLIDER (self)); + + for (i = 0; i < priv->children->len; i++) + { + DzlSliderChild *child; + gint margin; + + child = g_ptr_array_index (priv->children, i); + + switch (child->position) + { + case DZL_SLIDER_NONE: + break; + + case DZL_SLIDER_BOTTOM: + gtk_widget_get_preferred_height (child->widget, NULL, &margin); + real_bottom_margin = MAX (real_bottom_margin, margin); + break; + + case DZL_SLIDER_TOP: + gtk_widget_get_preferred_height (child->widget, NULL, &margin); + real_top_margin = MAX (real_top_margin, margin); + break; + + case DZL_SLIDER_LEFT: + gtk_widget_get_preferred_width (child->widget, NULL, &margin); + real_left_margin = MAX (real_left_margin, margin); + break; + + case DZL_SLIDER_RIGHT: + gtk_widget_get_preferred_width (child->widget, NULL, &margin); + real_right_margin = MAX (real_right_margin, margin); + break; + + default: + g_assert_not_reached (); + break; + } + } + + x_ratio = gtk_adjustment_get_value (priv->h_adj); + y_ratio = gtk_adjustment_get_value (priv->v_adj); + + if (x_ratio < 0.0) + *x_margin = x_ratio * real_left_margin; + else if (x_ratio > 0.0) + *x_margin = x_ratio * real_right_margin; + else + *x_margin = 0; + + if (y_ratio < 0.0) + *y_margin = y_ratio * real_bottom_margin; + else if (y_ratio > 0.0) + *y_margin = y_ratio * real_top_margin; + else + *y_margin = 0; +} + +static void +dzl_slider_compute_child_allocation (DzlSlider *self, + DzlSliderChild *child, + GtkAllocation *window_allocation, + GtkAllocation *child_allocation) +{ + GtkAllocation real_window_allocation; + GtkAllocation real_child_allocation; + gint nat_height; + gint nat_width; + gint x_margin; + gint y_margin; + + g_assert (DZL_IS_SLIDER (self)); + g_assert (child != NULL); + g_assert (GTK_IS_WIDGET (child->widget)); + + gtk_widget_get_allocation (GTK_WIDGET (self), &real_window_allocation); + + dzl_slider_compute_margin (self, &x_margin, &y_margin); + + if (child->position == DZL_SLIDER_NONE) + { + real_child_allocation.y = y_margin; + real_child_allocation.x = x_margin; + real_child_allocation.width = real_window_allocation.width; + real_child_allocation.height = real_window_allocation.height; + } + else if (child->position == DZL_SLIDER_TOP) + { + gtk_widget_get_preferred_height (child->widget, NULL, &nat_height); + + real_window_allocation.y = real_window_allocation.y - nat_height + y_margin; + real_window_allocation.height = nat_height; + + real_child_allocation.y = 0; + real_child_allocation.x = 0; + real_child_allocation.height = nat_height; + real_child_allocation.width = real_window_allocation.width; + } + else if (child->position == DZL_SLIDER_BOTTOM) + { + gtk_widget_get_preferred_height (child->widget, NULL, &nat_height); + + real_window_allocation.y = real_window_allocation.y + real_window_allocation.height + y_margin; + real_window_allocation.height = nat_height; + + real_child_allocation.y = 0; + real_child_allocation.x = 0; + real_child_allocation.height = nat_height; + real_child_allocation.width = real_window_allocation.width; + } + else if (child->position == DZL_SLIDER_RIGHT) + { + gtk_widget_get_preferred_width (child->widget, NULL, &nat_width); + + real_window_allocation.x = real_window_allocation.x + real_window_allocation.width + x_margin; + real_window_allocation.width = nat_width; + + real_child_allocation.y = 0; + real_child_allocation.x = 0; + real_child_allocation.height = real_window_allocation.height; + real_child_allocation.width = nat_width; + } + else if (child->position == DZL_SLIDER_LEFT) + { + gtk_widget_get_preferred_width (child->widget, NULL, &nat_width); + + real_window_allocation.x = real_window_allocation.x - nat_width + x_margin; + real_window_allocation.width = nat_width; + + real_child_allocation.y = 0; + real_child_allocation.x = 0; + real_child_allocation.height = real_window_allocation.height; + real_child_allocation.width = nat_width; + } + else + { + g_assert_not_reached (); + } + + if (window_allocation) + *window_allocation = real_window_allocation; + + if (child_allocation) + *child_allocation = real_child_allocation; +} + +static GdkWindow * +dzl_slider_create_child_window (DzlSlider *self, + DzlSliderChild *child) +{ + GtkWidget *widget = (GtkWidget *)self; + GdkWindow *window; + GtkAllocation allocation; + GdkWindowAttr attributes; + gint attributes_mask; + + g_assert (DZL_IS_SLIDER (self)); + g_assert (child != NULL); + + dzl_slider_compute_child_allocation (self, child, &allocation, NULL); + + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.width = allocation.width; + attributes.height = allocation.height; + attributes.x = allocation.x; + attributes.y = allocation.y; + attributes.visual = gtk_widget_get_visual (widget); + attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL; + attributes.event_mask = gtk_widget_get_events (widget) | GDK_EXPOSURE_MASK; + + window = gdk_window_new (gtk_widget_get_window (widget), &attributes, attributes_mask); + gtk_widget_register_window (widget, window); + + gtk_widget_set_parent_window (child->widget, window); + + return window; +} + +static void +dzl_slider_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlSlider *self = (DzlSlider *)container; + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + DzlSliderChild *child; + + g_assert (DZL_IS_SLIDER (self)); + g_assert (GTK_IS_WIDGET (widget)); + + child = g_slice_new0 (DzlSliderChild); + child->position = DZL_SLIDER_NONE; + child->widget = g_object_ref (widget); + + g_ptr_array_add (priv->children, child); + + gtk_widget_set_parent (widget, GTK_WIDGET (self)); + + if (gtk_widget_get_realized (GTK_WIDGET (self))) + child->window = dzl_slider_create_child_window (self, child); +} + +static void +dzl_slider_remove (GtkContainer *container, + GtkWidget *widget) +{ + DzlSlider *self = (DzlSlider *)container; + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + DzlSliderChild *child; + gsize i; + + g_assert (DZL_IS_SLIDER (self)); + g_assert (GTK_IS_WIDGET (widget)); + + for (i = 0; i < priv->children->len; i++) + { + child = g_ptr_array_index (priv->children, i); + + if (child->widget == widget) + { + gtk_widget_unparent (widget); + g_ptr_array_remove_index (priv->children, i); + gtk_widget_queue_allocate (GTK_WIDGET (self)); + break; + } + } +} + +static void +dzl_slider_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + DzlSlider *self = (DzlSlider *)widget; + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + guint i; + + g_assert (DZL_IS_SLIDER (self)); + g_assert (allocation != NULL); + + gtk_widget_set_allocation (widget, allocation); + + for (i = 0; i < priv->children->len; i++) + { + DzlSliderChild *child = g_ptr_array_index (priv->children, i); + + if (gtk_widget_get_mapped (child->widget)) + { + GtkAllocation window_allocation; + GtkAllocation child_allocation; + + dzl_slider_compute_child_allocation (self, child, &window_allocation, &child_allocation); + + gdk_window_move_resize (child->window, + window_allocation.x, + window_allocation.y, + window_allocation.width, + window_allocation.height); + + /* raise the window edges */ + if (child->position != DZL_SLIDER_NONE) + gdk_window_show (child->window); + + gtk_widget_size_allocate (child->widget, &child_allocation); + } + } +} + +static void +dzl_slider_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + DzlSlider *self = (DzlSlider *)container; + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + GtkWidget **children; + guint len; + guint i; + + g_assert (DZL_IS_SLIDER (self)); + + /* + * We need to be widget re-entrant safe, meaning that the callback could + * remove a child during callback(), using gtk_widget_destroy or similar. So + * we create a local array containing a ref'd copy of all of the widgets in + * case the callback removes widgets. + */ + + len = priv->children->len; + children = g_new0 (GtkWidget *, len); + + for (i = 0; i < len; i++) + { + DzlSliderChild *child = g_ptr_array_index (priv->children, i); + + children [i] = g_object_ref (child->widget); + } + + for (i = 0; i < len; i++) + { + callback (children [i], callback_data); + g_object_unref (children [i]); + } + + g_free (children); +} + +static DzlSliderChild * +dzl_slider_get_child (DzlSlider *self, + GtkWidget *widget) +{ + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + gsize i; + + g_assert (DZL_IS_SLIDER (self)); + g_assert (GTK_IS_WIDGET (widget)); + g_assert (gtk_widget_get_parent (widget) == GTK_WIDGET (self)); + + for (i = 0; i < priv->children->len; i++) + { + DzlSliderChild *child; + + child = g_ptr_array_index (priv->children, i); + + if (child->widget == widget) + return child; + } + + g_assert_not_reached (); + + return NULL; +} + +static DzlSliderPosition +dzl_slider_child_get_position (DzlSlider *self, + GtkWidget *widget) +{ + DzlSliderChild *child; + + g_assert (DZL_IS_SLIDER (self)); + g_assert (GTK_IS_WIDGET (widget)); + + child = dzl_slider_get_child (self, widget); + + return child->position; +} + +static void +dzl_slider_child_set_position (DzlSlider *self, + GtkWidget *widget, + DzlSliderPosition position) +{ + DzlSliderChild *child; + + g_assert (DZL_IS_SLIDER (self)); + g_assert (GTK_IS_WIDGET (widget)); + g_assert (position >= DZL_SLIDER_NONE); + g_assert (position <= DZL_SLIDER_LEFT); + + child = dzl_slider_get_child (self, widget); + + if (position != child->position) + { + child->position = position; + gtk_container_child_notify (GTK_CONTAINER (self), widget, "position"); + gtk_widget_queue_allocate (GTK_WIDGET (self)); + } +} + +static void +dzl_slider_get_child_property (GtkContainer *container, + GtkWidget *child, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSlider *self = (DzlSlider *)container; + + switch (prop_id) + { + case CHILD_PROP_POSITION: + g_value_set_enum (value, dzl_slider_child_get_position (self, child)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_slider_set_child_property (GtkContainer *container, + GtkWidget *child, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSlider *self = (DzlSlider *)container; + + switch (prop_id) + { + case CHILD_PROP_POSITION: + dzl_slider_child_set_position (self, child, g_value_get_enum (value)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_slider_get_preferred_height (GtkWidget *widget, + gint *min_height, + gint *nat_height) +{ + DzlSlider *self = (DzlSlider *)widget; + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + gint real_min_height = 0; + gint real_nat_height = 0; + gsize i; + + g_assert (DZL_IS_SLIDER (self)); + + for (i = 0; i < priv->children->len; i++) + { + DzlSliderChild *child; + gint child_min_height = 0; + gint child_nat_height = 0; + + child = g_ptr_array_index (priv->children, i); + + if ((child->position == DZL_SLIDER_NONE) && gtk_widget_get_visible (child->widget)) + { + gtk_widget_get_preferred_height (child->widget, &child_min_height, &child_nat_height); + real_min_height = MAX (real_min_height, child_min_height); + real_nat_height = MAX (real_nat_height, child_nat_height); + } + } + + *min_height = real_min_height; + *nat_height = real_nat_height; +} + +static void +dzl_slider_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + DzlSlider *self = (DzlSlider *)widget; + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + gint real_min_width = 0; + gint real_nat_width = 0; + gsize i; + + g_assert (DZL_IS_SLIDER (self)); + + for (i = 0; i < priv->children->len; i++) + { + DzlSliderChild *child; + gint child_min_width = 0; + gint child_nat_width = 0; + + child = g_ptr_array_index (priv->children, i); + + if ((child->position == DZL_SLIDER_NONE) && gtk_widget_get_visible (child->widget)) + { + gtk_widget_get_preferred_width (child->widget, &child_min_width, &child_nat_width); + real_min_width = MAX (real_min_width, child_min_width); + real_nat_width = MAX (real_nat_width, child_nat_width); + } + } + + *min_width = real_min_width; + *nat_width = real_nat_width; +} + +static void +dzl_slider_realize (GtkWidget *widget) +{ + DzlSlider *self = (DzlSlider *)widget; + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + GdkWindow *window; + gsize i; + + g_assert (DZL_IS_SLIDER (self)); + + gtk_widget_set_realized (widget, TRUE); + + window = gtk_widget_get_parent_window (widget); + gtk_widget_set_window (widget, g_object_ref (window)); + + for (i = 0; i < priv->children->len; i++) + { + DzlSliderChild *child; + + child = g_ptr_array_index (priv->children, i); + + if (child->window == NULL) + child->window = dzl_slider_create_child_window (self, child); + } +} + +static void +dzl_slider_unrealize (GtkWidget *widget) +{ + DzlSlider *self = (DzlSlider *)widget; + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + gsize i; + + g_assert (DZL_IS_SLIDER (self)); + + for (i = 0; i < priv->children->len; i++) + { + DzlSliderChild *child; + + child = g_ptr_array_index (priv->children, i); + + if (child->window != NULL) + { + gtk_widget_set_parent_window (child->widget, NULL); + gtk_widget_unregister_window (widget, child->window); + gdk_window_destroy (child->window); + child->window = NULL; + } + } + + GTK_WIDGET_CLASS (dzl_slider_parent_class)->unrealize (widget); +} + +static void +dzl_slider_map (GtkWidget *widget) +{ + DzlSlider *self = (DzlSlider *)widget; + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + gsize i; + + g_assert (DZL_IS_SLIDER (self)); + + GTK_WIDGET_CLASS (dzl_slider_parent_class)->map (widget); + + for (i = 0; i < priv->children->len; i++) + { + DzlSliderChild *child; + + child = g_ptr_array_index (priv->children, i); + + if ((child->window != NULL) && + gtk_widget_get_visible (child->widget) && + gtk_widget_get_child_visible (child->widget)) + gdk_window_show (child->window); + } +} + +static void +dzl_slider_unmap (GtkWidget *widget) +{ + DzlSlider *self = (DzlSlider *)widget; + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + gsize i; + + g_assert (DZL_IS_SLIDER (self)); + + for (i = 0; i < priv->children->len; i++) + { + DzlSliderChild *child; + + child = g_ptr_array_index (priv->children, i); + + if ((child->window != NULL) && gdk_window_is_visible (child->window)) + gdk_window_hide (child->window); + } + + GTK_WIDGET_CLASS (dzl_slider_parent_class)->unmap (widget); +} + +static void +dzl_slider_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + DzlSliderPosition position = DZL_SLIDER_NONE; + DzlSlider *self = (DzlSlider *)buildable; + + g_assert (DZL_IS_SLIDER (self)); + g_assert (GTK_IS_BUILDABLE (buildable)); + g_assert (GTK_IS_BUILDER (builder)); + g_assert (G_IS_OBJECT (child)); + + if (!GTK_IS_WIDGET (child)) + { + g_warning ("Child \"%s\" must be of type GtkWidget.", + G_OBJECT_TYPE_NAME (child)); + return; + } + + if (type == NULL) + position = DZL_SLIDER_NONE; + else if (g_str_equal (type, "bottom")) + position = DZL_SLIDER_BOTTOM; + else if (g_str_equal (type, "top")) + position = DZL_SLIDER_TOP; + else if (g_str_equal (type, "left")) + position = DZL_SLIDER_LEFT; + else if (g_str_equal (type, "right")) + position = DZL_SLIDER_RIGHT; + else + g_warning ("Unknown child type \"%s\"", type); + + dzl_slider_add_slider (self, GTK_WIDGET (child), position); +} + +static void +dzl_slider_finalize (GObject *object) +{ + DzlSlider *self = (DzlSlider *)object; + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + + g_clear_object (&priv->h_adj); + g_clear_object (&priv->v_adj); + g_clear_pointer (&priv->children, g_ptr_array_unref); + + dzl_clear_weak_pointer (&priv->h_anim); + dzl_clear_weak_pointer (&priv->v_anim); + + G_OBJECT_CLASS (dzl_slider_parent_class)->finalize (object); +} + +static void +dzl_slider_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlSlider *self = DZL_SLIDER (object); + + switch (prop_id) + { + case PROP_POSITION: + g_value_set_enum (value, dzl_slider_get_position (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_slider_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlSlider *self = DZL_SLIDER (object); + + switch (prop_id) + { + case PROP_POSITION: + dzl_slider_set_position (self, g_value_get_enum (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +buildable_iface_init (GtkBuildableIface *iface) +{ + iface->add_child = dzl_slider_add_child; +} + +static void +dzl_slider_class_init (DzlSliderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->finalize = dzl_slider_finalize; + object_class->get_property = dzl_slider_get_property; + object_class->set_property = dzl_slider_set_property; + + widget_class->get_preferred_height = dzl_slider_get_preferred_height; + widget_class->get_preferred_width = dzl_slider_get_preferred_width; + widget_class->map = dzl_slider_map; + widget_class->realize = dzl_slider_realize; + widget_class->size_allocate = dzl_slider_size_allocate; + widget_class->unmap = dzl_slider_unmap; + widget_class->unrealize = dzl_slider_unrealize; + + container_class->add = dzl_slider_add; + container_class->forall = dzl_slider_forall; + container_class->get_child_property = dzl_slider_get_child_property; + container_class->remove = dzl_slider_remove; + container_class->set_child_property = dzl_slider_set_child_property; + + properties [PROP_POSITION] = + g_param_spec_enum ("position", + "Position", + "Which slider child is visible.", + DZL_TYPE_SLIDER_POSITION, + DZL_SLIDER_NONE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + gtk_container_class_install_child_property (container_class, + CHILD_PROP_POSITION, + g_param_spec_enum ("position", + "Position", + "Position", + DZL_TYPE_SLIDER_POSITION, + DZL_SLIDER_NONE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); +} + +static void +dzl_slider_init (DzlSlider *self) +{ + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + + priv->position = DZL_SLIDER_NONE; + priv->children = g_ptr_array_new_with_free_func ((GDestroyNotify)dzl_slider_child_free); + + priv->v_adj = g_object_new (GTK_TYPE_ADJUSTMENT, + "lower", -1.0, + "upper", 1.0, + "value", 0.0, + NULL); + g_signal_connect_object (priv->v_adj, + "value-changed", + G_CALLBACK (gtk_widget_queue_allocate), + self, + G_CONNECT_SWAPPED); + + priv->h_adj = g_object_new (GTK_TYPE_ADJUSTMENT, + "lower", -1.0, + "upper", 1.0, + "value", 0.0, + NULL); + g_signal_connect_object (priv->h_adj, + "value-changed", + G_CALLBACK (gtk_widget_queue_allocate), + self, + G_CONNECT_SWAPPED); + + gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); +} + +GType +dzl_slider_position_get_type (void) +{ + static GType type_id; + static const GEnumValue values[] = { + { DZL_SLIDER_NONE, "DZL_SLIDER_NONE", "none" }, + { DZL_SLIDER_TOP, "DZL_SLIDER_TOP", "top" }, + { DZL_SLIDER_RIGHT, "DZL_SLIDER_RIGHT", "right" }, + { DZL_SLIDER_BOTTOM, "DZL_SLIDER_BOTTOM", "bottom" }, + { DZL_SLIDER_LEFT, "DZL_SLIDER_LEFT", "left" }, + { 0 } + }; + + if (g_once_init_enter (&type_id)) + { + GType _type_id; + + _type_id = g_enum_register_static ("DzlSliderPosition", values); + g_once_init_leave (&type_id, _type_id); + } + + return type_id; +} + +GtkWidget * +dzl_slider_new (void) +{ + return g_object_new (DZL_TYPE_SLIDER, NULL); +} + +DzlSliderPosition +dzl_slider_get_position (DzlSlider *self) +{ + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_SLIDER (self), DZL_SLIDER_NONE); + + return priv->position; +} + +void +dzl_slider_set_position (DzlSlider *self, + DzlSliderPosition position) +{ + DzlSliderPrivate *priv = dzl_slider_get_instance_private (self); + + g_return_if_fail (DZL_IS_SLIDER (self)); + g_return_if_fail (position >= DZL_SLIDER_NONE); + g_return_if_fail (position <= DZL_SLIDER_LEFT); + + if (priv->position != position) + { + GdkFrameClock *frame_clock; + DzlAnimation *anim; + gdouble v_value; + gdouble h_value; + + priv->position = position; + + if (priv->h_anim) + dzl_animation_stop (priv->h_anim); + dzl_clear_weak_pointer (&priv->h_anim); + + if (priv->v_anim) + dzl_animation_stop (priv->v_anim); + dzl_clear_weak_pointer (&priv->v_anim); + + switch (position) + { + case DZL_SLIDER_NONE: + h_value = 0.0; + v_value = 0.0; + break; + + case DZL_SLIDER_TOP: + h_value = 0.0; + v_value = 1.0; + break; + + case DZL_SLIDER_RIGHT: + h_value = -1.0; + v_value = 0.0; + break; + + case DZL_SLIDER_BOTTOM: + h_value = 0.0; + v_value = -1.0; + break; + + case DZL_SLIDER_LEFT: + h_value = 1.0; + v_value = 0.0; + break; + + default: + g_return_if_reached (); + } + + frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self)); + + anim = dzl_object_animate (priv->h_adj, + ANIMATION_MODE, + ANIMATION_DURATION, + frame_clock, + "value", h_value, + NULL); + dzl_set_weak_pointer (&priv->h_anim, anim); + + anim = dzl_object_animate (priv->v_adj, + ANIMATION_MODE, + ANIMATION_DURATION, + frame_clock, + "value", v_value, + NULL); + dzl_set_weak_pointer (&priv->v_anim, anim); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSITION]); + gtk_widget_queue_allocate (GTK_WIDGET (self)); + } +} + +void +dzl_slider_add_slider (DzlSlider *self, + GtkWidget *widget, + DzlSliderPosition position) +{ + g_return_if_fail (DZL_IS_SLIDER (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (position >= DZL_SLIDER_NONE); + g_return_if_fail (position <= DZL_SLIDER_LEFT); + + gtk_container_add_with_properties (GTK_CONTAINER (self), widget, + "position", position, + NULL); +} diff --git a/src/widgets/dzl-slider.h b/src/widgets/dzl-slider.h new file mode 100644 index 0000000..4f01630 --- /dev/null +++ b/src/widgets/dzl-slider.h @@ -0,0 +1,64 @@ +/* dzl-slider.h + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_SLIDER_H +#define DZL_SLIDER_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_SLIDER (dzl_slider_get_type()) +#define DZL_TYPE_SLIDER_POSITION (dzl_slider_position_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlSlider, dzl_slider, DZL, SLIDER, GtkContainer) + +typedef enum +{ + DZL_SLIDER_NONE, + DZL_SLIDER_TOP, + DZL_SLIDER_RIGHT, + DZL_SLIDER_BOTTOM, + DZL_SLIDER_LEFT, +} DzlSliderPosition; + +struct _DzlSliderClass +{ + GtkContainerClass parent_instance; +}; + +DZL_AVAILABLE_IN_ALL +GType dzl_slider_position_get_type (void); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_slider_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_slider_add_slider (DzlSlider *self, + GtkWidget *widget, + DzlSliderPosition position); +DZL_AVAILABLE_IN_ALL +DzlSliderPosition dzl_slider_get_position (DzlSlider *self); +DZL_AVAILABLE_IN_ALL +void dzl_slider_set_position (DzlSlider *self, + DzlSliderPosition position); + +G_END_DECLS + +#endif /* DZL_SLIDER_H */ diff --git a/src/widgets/dzl-stack-list.c b/src/widgets/dzl-stack-list.c new file mode 100644 index 0000000..df229ff --- /dev/null +++ b/src/widgets/dzl-stack-list.c @@ -0,0 +1,672 @@ +/* dzl-stack-list.c + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-stack-list" + +#include "config.h" + +#include + +#include "animation/dzl-animation.h" +#include "util/dzl-util-private.h" +#include "widgets/dzl-rect-helper.h" +#include "widgets/dzl-stack-list.h" +#include "util/dzl-gtk.h" + +#define FADE_DURATION 250 +#define SLIDE_DURATION_MAX 300 + +typedef struct +{ + GtkOverlay *overlay; + GtkScrolledWindow *scroller; + GtkBox *box; + GtkListBox *headers; + GtkListBox *content; + GtkListBox *fake_list; + GtkStack *flip_stack; + + GPtrArray *models; + + GtkListBoxRow *activated; + + GtkListBoxRow *animating; + DzlAnimation *animation; + DzlRectHelper *animating_rect; +} DzlStackListPrivate; + +typedef struct +{ + GListModel *model; + GtkWidget *header; + DzlStackListCreateWidgetFunc create_widget_func; + gpointer user_data; + GDestroyNotify user_data_free_func; +} ModelInfo; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlStackList, dzl_stack_list, GTK_TYPE_BIN) + +enum { + PROP_0, + PROP_MODEL, + LAST_PROP +}; + +enum { + HEADER_ACTIVATED, + ROW_ACTIVATED, + LAST_SIGNAL +}; + +static GParamSpec *properties [LAST_PROP]; +static guint signals [LAST_SIGNAL]; + +static void +model_info_free (gpointer data) +{ + ModelInfo *info = data; + + g_object_unref (info->model); + if (info->user_data_free_func) + info->user_data_free_func (info->user_data); + g_slice_free (ModelInfo, info); +} + +static void +enable_activatable (GtkWidget *widget, + gpointer user_data) +{ + GtkWidget **last = user_data; + + g_assert (GTK_IS_LIST_BOX_ROW (widget)); + g_assert (*last == NULL || GTK_IS_WIDGET (*last)); + + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (widget), TRUE); + *last = widget; +} + +static void +dzl_stack_list_update_activatables (DzlStackList *self) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + GtkWidget *last = NULL; + + g_assert (DZL_IS_STACK_LIST (self)); + + gtk_container_foreach (GTK_CONTAINER (priv->headers), + enable_activatable, + &last); + + if (GTK_IS_LIST_BOX_ROW (last)) + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (last), FALSE); +} + +static GtkWidget * +dzl_stack_list_create_widget_func (gpointer item, + gpointer user_data) +{ + ModelInfo *info = user_data; + + return info->create_widget_func (item, info->user_data); +} + +static void +dzl_stack_list_content_row_activated (DzlStackList *self, + GtkListBoxRow *row, + GtkListBox *box) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + + g_return_if_fail (DZL_IS_STACK_LIST (self)); + g_return_if_fail (GTK_IS_LIST_BOX_ROW (row)); + g_return_if_fail (GTK_IS_LIST_BOX (box)); + + priv->activated = row; + + g_signal_emit (self, signals [ROW_ACTIVATED], 0, row); + + priv->activated = NULL; +} + +static void +dzl_stack_list_header_row_activated (DzlStackList *self, + GtkListBoxRow *row, + GtkListBox *box) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + + g_return_if_fail (DZL_IS_STACK_LIST (self)); + g_return_if_fail (GTK_IS_LIST_BOX_ROW (row)); + g_return_if_fail (GTK_IS_LIST_BOX (box)); + + priv->activated = row; + + g_signal_emit (self, signals [HEADER_ACTIVATED], 0, row); + + priv->activated = NULL; +} + +static gboolean +dzl_stack_list__overlay__get_child_position (DzlStackList *self, + GtkWidget *widget, + GdkRectangle *rect, + GtkOverlay *overlay) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + GtkRequisition min, nat; + + g_assert (DZL_IS_STACK_LIST (self)); + g_assert (GTK_IS_WIDGET (widget)); + g_assert (rect != NULL); + g_assert (GTK_IS_OVERLAY (overlay)); + + gtk_widget_get_preferred_size (widget, &min, &nat); + + dzl_rect_helper_get_rect (priv->animating_rect, rect); + + if (rect->width < min.width) + rect->width = min.width; + + if (rect->height < min.height) + rect->height = min.height; + + return TRUE; +} + +static void +dzl_stack_list_scroll_to_top (DzlStackList *self) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + GtkAdjustment *vadj; + + g_assert (DZL_IS_STACK_LIST (self)); + + vadj = gtk_scrolled_window_get_vadjustment (priv->scroller); + + gtk_adjustment_set_value (vadj, 0.0); +} + +static void +dzl_stack_list_end_anim (DzlStackList *self) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + GtkListBoxRow *header; + ModelInfo *info; + + g_assert (DZL_IS_STACK_LIST (self)); + g_assert (priv->animating != NULL); + g_assert (priv->models->len > 0); + + info = g_ptr_array_index (priv->models, priv->models->len - 1); + header = g_object_ref (priv->animating); + + dzl_gtk_widget_remove_style_class (GTK_WIDGET (header), "animating"); + + priv->animating = NULL; + + if (priv->animation != NULL) + { + dzl_animation_stop (priv->animation); + g_clear_object (&priv->animation); + } + + g_assert (header != NULL); + g_assert (GTK_IS_LIST_BOX_ROW (header)); + g_assert (gtk_widget_get_parent (GTK_WIDGET (header)) == GTK_WIDGET (priv->overlay)); + + gtk_container_remove (GTK_CONTAINER (priv->overlay), + GTK_WIDGET (header)); + + gtk_container_add (GTK_CONTAINER (priv->headers), GTK_WIDGET (header)); + + gtk_list_box_bind_model (priv->content, + info->model, + dzl_stack_list_create_widget_func, + info, + NULL); + + dzl_stack_list_scroll_to_top (self); + + gtk_stack_set_transition_type (GTK_STACK (priv->flip_stack), GTK_STACK_TRANSITION_TYPE_SLIDE_DOWN); + gtk_stack_set_visible_child (GTK_STACK (priv->flip_stack), GTK_WIDGET (priv->scroller)); + + dzl_stack_list_update_activatables (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]); + + g_object_unref (header); +} + +static void +animation_finished (gpointer data) +{ + DzlStackListPrivate *priv; + DzlStackList *self; + GtkListBoxRow *row; + gpointer *closure = data; + + g_assert (closure != NULL); + g_assert (DZL_IS_STACK_LIST (closure [0])); + g_assert (GTK_IS_LIST_BOX_ROW (closure [1])); + + self = closure [0]; + row = closure [1]; + + priv = dzl_stack_list_get_instance_private (self); + + if (row == priv->animating) + dzl_stack_list_end_anim (self); + + g_object_unref (closure[0]); + g_object_unref (closure[1]); + g_free (closure); +} + +static void +dzl_stack_list_begin_anim (DzlStackList *self, + GtkListBoxRow *row, + const GdkRectangle *begin_area, + const GdkRectangle *end_area) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + GdkFrameClock *frame_clock; + gpointer *closure; + guint pos; + guint duration = 0; + + g_assert (DZL_IS_STACK_LIST (self)); + g_assert (row != NULL); + g_assert (begin_area != NULL); + g_assert (end_area != NULL); + + priv->animating = row; + + dzl_gtk_widget_add_style_class (GTK_WIDGET (row), "animating"); + + g_object_set (priv->animating_rect, + "x", begin_area->x, + "y", begin_area->y, + "width", begin_area->width, + "height", begin_area->height, + NULL); + + frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self)); + + closure = g_new0 (gpointer, 2); + closure [0] = g_object_ref (self); + closure [1] = g_object_ref_sink (row); + + gtk_overlay_add_overlay (GTK_OVERLAY (priv->overlay), GTK_WIDGET (row)); + + pos = gtk_list_box_row_get_index (row); + + if (pos != 0) + { + GdkMonitor *monitor; + GdkDisplay *display; + GdkWindow *window; + guint distance = ABS (end_area->y - begin_area->y); + + display = gtk_widget_get_display (GTK_WIDGET (self)); + window = gtk_widget_get_window (GTK_WIDGET (self)); + monitor = gdk_display_get_monitor_at_window (display, window); + + duration = dzl_animation_calculate_duration (monitor, 0, distance); + duration = MIN (duration, SLIDE_DURATION_MAX); + } + + priv->animation = dzl_object_animate_full (priv->animating_rect, + DZL_ANIMATION_EASE_IN_OUT_CUBIC, + duration, + frame_clock, + animation_finished, + closure, + "x", end_area->x, + "y", end_area->y, + "width", end_area->width, + "height", end_area->height, + NULL); + + g_object_ref (priv->animation); + + g_signal_connect_object (priv->animating_rect, + "notify", + G_CALLBACK (gtk_widget_queue_resize), + priv->animating, + G_CONNECT_SWAPPED); + + gtk_stack_set_transition_type (GTK_STACK (priv->flip_stack), GTK_STACK_TRANSITION_TYPE_CROSSFADE); + gtk_stack_set_visible_child (GTK_STACK (priv->flip_stack), GTK_WIDGET (priv->fake_list)); +} + +static void +dzl_stack_list_real_header_activated (DzlStackList *self, + GtkListBoxRow *header) +{ + gint pos; + + g_assert (DZL_IS_STACK_LIST (self)); + g_assert (GTK_IS_LIST_BOX_ROW (header)); + + pos = gtk_list_box_row_get_index (header) + 1; + + while (dzl_stack_list_get_depth (self) > (guint)pos) + dzl_stack_list_pop (self); +} + +static void +dzl_stack_list_finalize (GObject *object) +{ + DzlStackList *self = (DzlStackList *)object; + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + + g_clear_pointer (&priv->models, g_ptr_array_unref); + g_clear_object (&priv->animating_rect); + g_clear_object (&priv->animation); + + G_OBJECT_CLASS (dzl_stack_list_parent_class)->finalize (object); +} + +static void +dzl_stack_list_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlStackList *self = DZL_STACK_LIST (object); + + switch (prop_id) + { + case PROP_MODEL: + g_value_set_object (value, dzl_stack_list_get_model (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_stack_list_class_init (DzlStackListClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = dzl_stack_list_finalize; + object_class->get_property = dzl_stack_list_get_property; + + klass->header_activated = dzl_stack_list_real_header_activated; + + properties [PROP_MODEL] = + g_param_spec_object ("model", + _("Model"), + _("Model"), + G_TYPE_LIST_MODEL, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + signals [HEADER_ACTIVATED] = + g_signal_new ("header-activated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlStackListClass, header_activated), + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + GTK_TYPE_LIST_BOX_ROW); + + signals [ROW_ACTIVATED] = + g_signal_new ("row-activated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (DzlStackListClass, row_activated), + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + GTK_TYPE_LIST_BOX_ROW); + + gtk_widget_class_set_css_name (widget_class, "dzlstacklist"); +} + +static void +dzl_stack_list_init (DzlStackList *self) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + + priv->animating_rect = g_object_new (DZL_TYPE_RECT_HELPER, NULL); + + priv->models = g_ptr_array_new_with_free_func (model_info_free); + + priv->overlay = g_object_new (GTK_TYPE_OVERLAY, + "visible", TRUE, + NULL); + g_signal_connect_swapped (priv->overlay, + "get-child-position", + G_CALLBACK (dzl_stack_list__overlay__get_child_position), + self); + gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (priv->overlay)); + + priv->box = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_VERTICAL, + "vexpand", TRUE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (priv->overlay), GTK_WIDGET (priv->box)); + + priv->headers = g_object_new (GTK_TYPE_LIST_BOX, + "selection-mode", GTK_SELECTION_NONE, + "visible", TRUE, + NULL); + g_signal_connect_swapped (priv->headers, + "row-activated", + G_CALLBACK (dzl_stack_list_header_row_activated), + self); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->headers)), + "stack-header"); + gtk_container_add (GTK_CONTAINER (priv->box), GTK_WIDGET (priv->headers)); + + priv->flip_stack = g_object_new (GTK_TYPE_STACK, + "transition-duration", FADE_DURATION, + "transition-type", GTK_STACK_TRANSITION_TYPE_NONE, + "visible", TRUE, + "vexpand", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (priv->box), GTK_WIDGET (priv->flip_stack)); + + priv->scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW, + "shadow-type", GTK_SHADOW_NONE, + "vexpand", TRUE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (priv->flip_stack), GTK_WIDGET (priv->scroller)); + + priv->content = g_object_new (GTK_TYPE_LIST_BOX, + "visible", TRUE, + NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->content)), + "stack-children"); + g_signal_connect_object (priv->content, + "row-activated", + G_CALLBACK (dzl_stack_list_content_row_activated), + self, + G_CONNECT_SWAPPED); + gtk_container_add (GTK_CONTAINER (priv->scroller), GTK_WIDGET (priv->content)); + + priv->fake_list = g_object_new (GTK_TYPE_LIST_BOX, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (priv->flip_stack), GTK_WIDGET (priv->fake_list)); +} + +GtkWidget * +dzl_stack_list_new (void) +{ + return g_object_new (DZL_TYPE_STACK_LIST, NULL); +} + +void +dzl_stack_list_push (DzlStackList *self, + GtkWidget *header, + GListModel *model, + DzlStackListCreateWidgetFunc create_widget_func, + gpointer user_data, + GDestroyNotify user_data_free_func) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + ModelInfo *info; + GdkRectangle current_area; + GdkRectangle target_area; + + g_return_if_fail (DZL_IS_STACK_LIST (self)); + g_return_if_fail (GTK_IS_WIDGET (header)); + g_return_if_fail (G_IS_LIST_MODEL (model)); + g_return_if_fail (create_widget_func != NULL); + + if (priv->animating != NULL) + dzl_stack_list_end_anim (self); + + if (!GTK_IS_LIST_BOX_ROW (header)) + header = g_object_new (GTK_TYPE_LIST_BOX_ROW, + "child", header, + "visible", TRUE, + NULL); + + info = g_slice_new0 (ModelInfo); + info->header = header; + info->model = g_object_ref (model); + info->create_widget_func = create_widget_func; + info->user_data = user_data; + info->user_data_free_func = user_data_free_func; + + g_ptr_array_add (priv->models, info); + + /* + * Nothing to animate, make everything happen immediately. + */ + if (priv->activated == NULL) + { + gtk_container_add (GTK_CONTAINER (priv->headers), GTK_WIDGET (header)); + dzl_stack_list_update_activatables (self); + gtk_list_box_bind_model (priv->content, + model, + dzl_stack_list_create_widget_func, + info, + NULL); + dzl_stack_list_scroll_to_top (self); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]); + return; + } + + /* + * Get the location to begin the animation. + */ + gtk_widget_get_allocation (GTK_WIDGET (priv->activated), ¤t_area); + gtk_widget_translate_coordinates (GTK_WIDGET (priv->activated), + GTK_WIDGET (priv->overlay), + 0, 0, + ¤t_area.x, ¤t_area.y); + + /* + * Get the location to end the animation. + */ + gtk_widget_get_allocation (GTK_WIDGET (priv->headers), &target_area); + target_area.x = current_area.x; + target_area.y = target_area.height; + target_area.width = current_area.width; + target_area.height = current_area.height; + + dzl_stack_list_begin_anim (self, GTK_LIST_BOX_ROW (header), ¤t_area, &target_area); +} + +void +dzl_stack_list_pop (DzlStackList *self) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + ModelInfo *info; + + g_return_if_fail (DZL_IS_STACK_LIST (self)); + + if (priv->models->len == 0) + return; + + if (priv->animating != NULL) + dzl_stack_list_end_anim (self); + + info = g_ptr_array_index (priv->models, priv->models->len - 1); + + gtk_container_remove (GTK_CONTAINER (priv->headers), GTK_WIDGET (info->header)); + gtk_list_box_bind_model (priv->content, NULL, NULL, NULL, NULL); + g_ptr_array_remove_index (priv->models, priv->models->len - 1); + + if (priv->models->len > 0) + { + info = g_ptr_array_index (priv->models, priv->models->len - 1); + gtk_list_box_bind_model (priv->content, + info->model, + dzl_stack_list_create_widget_func, + info, + NULL); + } + + dzl_stack_list_update_activatables (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]); +} + +/** + * dzl_stack_list_get_model: + * + * Returns: (transfer none): An #DzlStackList. + */ +GListModel * +dzl_stack_list_get_model (DzlStackList *self) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + ModelInfo *info; + + g_return_val_if_fail (DZL_IS_STACK_LIST (self), NULL); + + if (priv->models->len == 0) + return NULL; + + info = g_ptr_array_index (priv->models, priv->models->len - 1); + + return info->model; +} + +guint +dzl_stack_list_get_depth (DzlStackList *self) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + + g_return_val_if_fail (DZL_IS_STACK_LIST (self), 0); + + return priv->models->len; +} + +void +dzl_stack_list_clear (DzlStackList *self) +{ + DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); + + g_return_if_fail (DZL_IS_STACK_LIST (self)); + + while (priv->models->len > 0) + dzl_stack_list_pop (self); +} diff --git a/src/widgets/dzl-stack-list.h b/src/widgets/dzl-stack-list.h new file mode 100644 index 0000000..9429f05 --- /dev/null +++ b/src/widgets/dzl-stack-list.h @@ -0,0 +1,85 @@ +/* dzl-stack-list.h + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_STACK_LIST_H +#define DZL_STACK_LIST_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_STACK_LIST (dzl_stack_list_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlStackList, dzl_stack_list, DZL, STACK_LIST, GtkBin) + +struct _DzlStackListClass +{ + GtkBinClass parent_instance; + + void (*row_activated) (DzlStackList *self, + GtkListBoxRow *row); + void (*header_activated) (DzlStackList *self, + GtkListBoxRow *row); + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +/** + * DzlStackListCreateWidgetFunc: + * @item: (type GObject): the item from the model for which to create a widget for + * @user_data: (closure): user data from dzl_stack_list_push() + * + * Called for stack lists that are bound to a #GListModel with + * dzl_stack_list_push() for each item that gets added to the model. + * + * Returns: (transfer full): a #GtkWidget that represents @item + */ +typedef GtkWidget *(*DzlStackListCreateWidgetFunc) (gpointer item, + gpointer user_data); + +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_stack_list_new (void); +DZL_AVAILABLE_IN_ALL +void dzl_stack_list_push (DzlStackList *self, + GtkWidget *header, + GListModel *model, + DzlStackListCreateWidgetFunc create_widget_func, + gpointer user_data, + GDestroyNotify user_data_free_func); +DZL_AVAILABLE_IN_ALL +void dzl_stack_list_pop (DzlStackList *self); +DZL_AVAILABLE_IN_ALL +GListModel *dzl_stack_list_get_model (DzlStackList *self); +DZL_AVAILABLE_IN_ALL +guint dzl_stack_list_get_depth (DzlStackList *self); +DZL_AVAILABLE_IN_ALL +void dzl_stack_list_clear (DzlStackList *self); + +G_END_DECLS + +#endif /* DZL_STACK_LIST_H */ diff --git a/src/widgets/dzl-three-grid.c b/src/widgets/dzl-three-grid.c new file mode 100644 index 0000000..2ede02a --- /dev/null +++ b/src/widgets/dzl-three-grid.c @@ -0,0 +1,806 @@ +/* dzl-three-grid.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#define G_LOG_DOMAIN "dzl-three-grid" + +#include "config.h" + +#include + +#include "dzl-three-grid.h" + +typedef struct +{ + GtkWidget *widget; + DzlThreeGridColumn column; + gint row; + gint min_height; + gint nat_height; + gint min_baseline; + gint nat_baseline; +} DzlThreeGridChild; + +typedef struct +{ + GPtrArray *children; + GHashTable *row_infos; + guint column_spacing; + guint row_spacing; +} DzlThreeGridPrivate; + +typedef struct +{ + gint row; + gint min_above_baseline; + gint min_below_baseline; + gint nat_above_baseline; + gint nat_below_baseline; +} DzlThreeGridRowInfo; + +G_DEFINE_TYPE_WITH_PRIVATE (DzlThreeGrid, dzl_three_grid, GTK_TYPE_CONTAINER) + +enum { + PROP_0, + PROP_COLUMN_SPACING, + PROP_ROW_SPACING, + N_PROPS +}; + +enum { + CHILD_PROP_0, + CHILD_PROP_ROW, + CHILD_PROP_COLUMN, + N_CHILD_PROPS +}; + +static GParamSpec *properties [N_PROPS]; +static GParamSpec *child_properties [N_CHILD_PROPS]; +static DzlThreeGridChild dummy; + +static DzlThreeGridChild * +dzl_three_grid_child_new (void) +{ + return g_slice_new0 (DzlThreeGridChild); +} + +static void +dzl_three_grid_child_free (gpointer data) +{ + DzlThreeGridChild *child = data; + + g_clear_object (&child->widget); + g_slice_free (DzlThreeGridChild, child); +} + +static DzlThreeGridChild * +dzl_three_grid_find_child (DzlThreeGrid *self, + GtkWidget *widget) +{ + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + + g_assert (DZL_IS_THREE_GRID (self)); + g_assert (GTK_IS_WIDGET (widget)); + + for (guint i = 0; i < priv->children->len; i++) + { + DzlThreeGridChild *child = g_ptr_array_index (priv->children, i); + + if (child->widget == widget) + return child; + } + + return &dummy; +} + +static void +dzl_three_grid_add (GtkContainer *container, + GtkWidget *widget) +{ + DzlThreeGrid *self = (DzlThreeGrid *)container; + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + DzlThreeGridChild *child; + + g_assert (DZL_IS_THREE_GRID (self)); + g_assert (GTK_IS_WIDGET (widget)); + + child = dzl_three_grid_child_new (); + child->widget = g_object_ref_sink (widget); + g_ptr_array_add (priv->children, child); + + gtk_widget_set_parent (widget, GTK_WIDGET (self)); +} + +static void +dzl_three_grid_remove (GtkContainer *container, + GtkWidget *widget) +{ + DzlThreeGrid *self = (DzlThreeGrid *)container; + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + + g_assert (DZL_IS_THREE_GRID (self)); + g_assert (GTK_IS_WIDGET (widget)); + + for (guint i = 0; i < priv->children->len; i++) + { + DzlThreeGridChild *child = g_ptr_array_index (priv->children, i); + + if (child->widget == widget) + { + gtk_widget_unparent (child->widget); + g_ptr_array_remove_index (priv->children, i); + gtk_widget_queue_resize (GTK_WIDGET (self)); + return; + } + } +} + +static GtkSizeRequestMode +dzl_three_grid_get_request_mode (GtkWidget *widget) +{ + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + +static void +dzl_three_grid_get_column_width (DzlThreeGrid *self, + DzlThreeGridColumn column, + gint *min_width, + gint *nat_width) +{ + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + gint real_min_width = 0; + gint real_nat_width = 0; + + g_assert (DZL_IS_THREE_GRID (self)); + g_assert (column >= DZL_THREE_GRID_COLUMN_LEFT); + g_assert (column <= DZL_THREE_GRID_COLUMN_RIGHT); + g_assert (min_width != NULL); + g_assert (nat_width != NULL); + + for (guint i = 0; i < priv->children->len; i++) + { + DzlThreeGridChild *child = g_ptr_array_index (priv->children, i); + + if (child->column == column) + { + gint child_min_width; + gint child_nat_width; + + gtk_widget_get_preferred_width (child->widget, &child_min_width, &child_nat_width); + + real_min_width = MAX (real_min_width, child_min_width); + real_nat_width = MAX (real_nat_width, child_nat_width); + } + } + + *min_width = real_min_width; + *nat_width = real_nat_width; +} + +static void +dzl_three_grid_get_preferred_width (GtkWidget *widget, + gint *min_width, + gint *nat_width) +{ + DzlThreeGrid *self = (DzlThreeGrid *)widget; + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + gint total_min_width = 0; + gint total_nat_width = 0; + gint min_widths[3]; + gint nat_widths[3]; + gint border_width; + + g_assert (DZL_IS_THREE_GRID (self)); + g_assert (min_width != NULL); + g_assert (nat_width != NULL); + + for (guint i = 0; i < 3; i++) + dzl_three_grid_get_column_width (self, i, &min_widths[i], &nat_widths[i]); + + total_min_width = MAX (min_widths[0], min_widths[2]) * 2 + min_widths[1]; + total_nat_width = MAX (nat_widths[0], nat_widths[2]) * 2 + nat_widths[1]; + + border_width = gtk_container_get_border_width (GTK_CONTAINER (self)); + + *min_width = total_min_width + (border_width * 2) + (priv->column_spacing * 2); + *nat_width = total_nat_width + (border_width * 2) + (priv->column_spacing * 2); +} + +static void +row_info_merge (DzlThreeGridRowInfo *row_info, + const DzlThreeGridRowInfo *other) +{ + g_assert (row_info); + g_assert (other); + + row_info->min_above_baseline = MAX (row_info->min_above_baseline, other->min_above_baseline); + row_info->min_below_baseline = MAX (row_info->min_below_baseline, other->min_below_baseline); + row_info->nat_above_baseline = MAX (row_info->nat_above_baseline, other->nat_above_baseline); + row_info->nat_below_baseline = MAX (row_info->nat_below_baseline, other->nat_below_baseline); +} + +static void +update_row_info (GHashTable *hashtable, + DzlThreeGridChild *child) +{ + GtkBaselinePosition baseline_position = GTK_BASELINE_POSITION_CENTER; + DzlThreeGridRowInfo *row_info; + DzlThreeGridRowInfo current = { 0 }; + + g_assert (hashtable); + g_assert (child); + + row_info = g_hash_table_lookup (hashtable, GINT_TO_POINTER (child->row)); + + if (row_info == NULL) + { + row_info = g_new0 (DzlThreeGridRowInfo, 1); + row_info->row = child->row; + g_hash_table_insert (hashtable, GINT_TO_POINTER (child->row), row_info); + } + + /* + * TODO: + * + * Allow setting baseline position per row. Right now we only support center + * because that is the easiest thing to start with. + */ + + if (child->min_baseline == -1) + { + if (baseline_position == GTK_BASELINE_POSITION_CENTER) + { + current.min_above_baseline = current.min_below_baseline = ceil (child->min_height / 2.0); + current.nat_above_baseline = current.nat_below_baseline = ceil (child->min_height / 2.0); + } + else if (baseline_position == GTK_BASELINE_POSITION_TOP) + { + g_assert_not_reached (); + } + else if (baseline_position == GTK_BASELINE_POSITION_BOTTOM) + { + g_assert_not_reached (); + } + } + else + { + current.min_above_baseline = child->min_baseline; + current.min_below_baseline = child->min_height - child->min_baseline; + current.nat_above_baseline = child->nat_baseline; + current.nat_below_baseline = child->nat_height - child->nat_baseline; + } + + row_info_merge (row_info, ¤t); +} + +static void +dzl_three_grid_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *min_height, + gint *nat_height) +{ + DzlThreeGrid *self = (DzlThreeGrid *)widget; + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + g_autoptr(GHashTable) row_infos = NULL; + DzlThreeGridRowInfo *row_info; + GHashTableIter iter; + gint real_min_height = 0; + gint real_nat_height = 0; + gint column_min_widths[3]; + gint column_nat_widths[3]; + gint widths[3]; + gint border_width; + gint n_rows; + + g_assert (DZL_IS_THREE_GRID (self)); + g_assert (min_height != NULL); + g_assert (nat_height != NULL); + + border_width = gtk_container_get_border_width (GTK_CONTAINER (self)); + width -= border_width * 2; + width -= priv->column_spacing * 2; + + dzl_three_grid_get_column_width (self, DZL_THREE_GRID_COLUMN_LEFT, &column_min_widths[0], &column_nat_widths[0]); + dzl_three_grid_get_column_width (self, DZL_THREE_GRID_COLUMN_CENTER, &column_min_widths[1], &column_nat_widths[1]); + dzl_three_grid_get_column_width (self, DZL_THREE_GRID_COLUMN_RIGHT, &column_min_widths[2], &column_nat_widths[2]); + + if ((MAX (column_min_widths[0], column_min_widths[2]) * 2 + column_nat_widths[1]) >= width) + { + widths[0] = column_min_widths[0]; + widths[2] = column_min_widths[2]; + widths[1] = width - widths[0] - widths[2]; + } + else + { + /* Handle #1 and #2 */ + widths[1] = column_nat_widths[1]; + widths[0] = (width - widths[1]) / 2; + widths[2] = width - widths[1] - widths[0]; + } + + row_infos = g_hash_table_new_full (NULL, NULL, NULL, g_free); + + for (guint i = 0; i < priv->children->len; i++) + { + DzlThreeGridChild *child = g_ptr_array_index (priv->children, i); + + if (!gtk_widget_get_visible (child->widget) || + !gtk_widget_get_child_visible (child->widget)) + continue; + + gtk_widget_get_preferred_height_and_baseline_for_width (child->widget, + widths[child->column], + &child->min_height, + &child->nat_height, + &child->min_baseline, + &child->nat_baseline); + update_row_info (row_infos, child); + } + + g_hash_table_iter_init (&iter, row_infos); + + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&row_info)) + { +#if 0 + g_print ("Row %d: MIN Above %d Below %d\n", + row_info->row, + row_info->min_above_baseline, row_info->min_below_baseline); + g_print ("Row %d: NAT Above %d Below %d\n", + row_info->row, + row_info->nat_above_baseline, row_info->nat_below_baseline); +#endif + real_min_height += row_info->min_above_baseline + row_info->min_below_baseline; + real_nat_height += row_info->nat_above_baseline + row_info->nat_below_baseline; + } + + real_min_height += border_width * 2; + real_nat_height += border_width * 2; + + n_rows = g_hash_table_size (row_infos); + + if (n_rows > 1) + { + real_min_height += (n_rows - 1) * priv->row_spacing; + real_nat_height += (n_rows - 1) * priv->row_spacing; + } + + *min_height = real_min_height; + *nat_height = real_nat_height; + +#if 0 + g_print ("%d children in %d rows: %dx%d\n", + priv->children->len, + g_hash_table_size (row_infos), + real_min_height, real_nat_height); +#endif + + g_clear_pointer (&priv->row_infos, g_hash_table_unref); + priv->row_infos = g_steal_pointer (&row_infos); +} + +static gint +sort_by_row (gconstpointer a, + gconstpointer b) +{ + const DzlThreeGridRowInfo *info_a = a; + const DzlThreeGridRowInfo *info_b = b; + + return info_a->row - info_b->row; +} + +static void +dzl_three_grid_size_allocate_children (DzlThreeGrid *self, + DzlThreeGridColumn column, + gint row, + GtkAllocation *allocation, + gint baseline) +{ + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + + g_assert (DZL_IS_THREE_GRID (self)); + g_assert (allocation != NULL); + + for (guint i = 0; i < priv->children->len; i++) + { + DzlThreeGridChild *child = g_ptr_array_index (priv->children, i); + + if (child->row == row && child->column == column) + { + GtkAllocation copy = *allocation; + gtk_widget_size_allocate_with_baseline (child->widget, ©, baseline); + } + } +} + +static void +dzl_three_grid_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + DzlThreeGrid *self = (DzlThreeGrid *)widget; + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + g_autofree GtkRequestedSize *rows = NULL; + const GList *iter; + GtkAllocation area; + GtkTextDirection dir; + GList *values; + guint i; + guint n_rows; + gint min_height; + gint nat_height; + gint border_width; + gint left_min_width; + gint left_nat_width; + gint center_min_width; + gint center_nat_width; + gint right_min_width; + gint right_nat_width; + gint left; + gint center; + gint right; + + g_assert (DZL_IS_THREE_GRID (self)); + g_assert (allocation != NULL); + + dir = gtk_widget_get_direction (widget); + + gtk_widget_set_allocation (widget, allocation); + + dzl_three_grid_get_preferred_height_for_width (widget, allocation->width, &min_height, &nat_height); + + if (min_height > allocation->height) + g_warning ("%s requested a minimum height of %d and got %d", + G_OBJECT_TYPE_NAME (widget), min_height, allocation->height); + + if (priv->row_infos == NULL) + return; + + values = g_hash_table_get_values (priv->row_infos); + values = g_list_sort (values, sort_by_row); + + area = *allocation; + border_width = gtk_container_get_border_width (GTK_CONTAINER (self)); + + area.x += border_width; + area.y += border_width; + area.width -= border_width * 2; + area.height -= border_width * 2; + + dzl_three_grid_get_column_width (self, DZL_THREE_GRID_COLUMN_LEFT, &left_min_width, &left_nat_width); + dzl_three_grid_get_column_width (self, DZL_THREE_GRID_COLUMN_CENTER, ¢er_min_width, ¢er_nat_width); + dzl_three_grid_get_column_width (self, DZL_THREE_GRID_COLUMN_RIGHT, &right_min_width, &right_nat_width); + + /* + * Determine how much to give to the center widget first. This is because we will + * just give the rest of the space on the sides to left/right columns and they + * can deal with alignment by using halign. + * + * We can be in one of a couple states: + * + * 1) There is enough room for all columns natural size. + * (We allocate the same to the left and the right). + * 2) There is enough for the natural size of the center + * but for some amount between natural and min sizing + * of the left/right columns. + * 3) There is only minimum size for columns and some + * amount between natural/minimum of the center. + * + * We can handle #1 and #2 with the same logic though. + */ + + if ((MAX (left_min_width, right_min_width) * 2 + center_nat_width) >= (area.width - (gint)(priv->column_spacing * 2))) + { + /* Handle #3 */ + left = left_min_width; + right = right_min_width; + center = area.width - left - right; + } + else + { + /* Handle #1 and #2 */ + center = center_nat_width; + right = left = (area.width - center) / 2 - priv->column_spacing; + } + + n_rows = g_list_length (values); + rows = g_new0 (GtkRequestedSize, n_rows); + + for (iter = values, i = 0; iter != NULL; iter = iter->next, i++) + { + DzlThreeGridRowInfo *row_info = iter->data; + + rows[i].data = row_info; + rows[i].minimum_size = row_info->min_above_baseline + row_info->min_below_baseline; + rows[i].natural_size = row_info->nat_above_baseline + row_info->nat_below_baseline; + } + + gtk_distribute_natural_allocation (area.height, n_rows, rows); + + for (i = 0; i < n_rows; i++) + { + GtkRequestedSize *size = &rows[i]; + DzlThreeGridRowInfo *row_info = size->data; + GtkAllocation child_alloc; + gint baseline; + + if (row_info->nat_above_baseline + row_info->nat_below_baseline <= size->minimum_size) + baseline = row_info->nat_above_baseline; + else + baseline = row_info->min_above_baseline; + + child_alloc.x = area.x; + child_alloc.width = left; + child_alloc.y = area.y; + child_alloc.height = size->minimum_size; + if (dir == GTK_TEXT_DIR_LTR) + dzl_three_grid_size_allocate_children (self, DZL_THREE_GRID_COLUMN_LEFT, row_info->row, &child_alloc, baseline); + else + dzl_three_grid_size_allocate_children (self, DZL_THREE_GRID_COLUMN_RIGHT, row_info->row, &child_alloc, baseline); + + child_alloc.x = area.x + left + priv->column_spacing; + child_alloc.width = center; + child_alloc.y = area.y; + child_alloc.height = size->minimum_size; + dzl_three_grid_size_allocate_children (self, DZL_THREE_GRID_COLUMN_CENTER, row_info->row, &child_alloc, baseline); + + child_alloc.x = area.x + area.width - right; + child_alloc.width = right; + child_alloc.y = area.y; + child_alloc.height = size->minimum_size; + if (dir == GTK_TEXT_DIR_LTR) + dzl_three_grid_size_allocate_children (self, DZL_THREE_GRID_COLUMN_RIGHT, row_info->row, &child_alloc, baseline); + else + dzl_three_grid_size_allocate_children (self, DZL_THREE_GRID_COLUMN_LEFT, row_info->row, &child_alloc, baseline); + + area.y += child_alloc.height + priv->row_spacing; + area.height -= child_alloc.height + priv->row_spacing; + } +} + +static void +dzl_three_grid_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer user_data) +{ + DzlThreeGrid *self = (DzlThreeGrid *)container; + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + + g_assert (GTK_IS_CONTAINER (self)); + g_assert (callback != NULL); + + for (guint i = priv->children->len; i > 0; i--) + { + DzlThreeGridChild *child = g_ptr_array_index (priv->children, i - 1); + + callback (child->widget, user_data); + } +} + +static void +dzl_three_grid_get_child_property (GtkContainer *container, + GtkWidget *widget, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlThreeGrid *self = (DzlThreeGrid *)container; + DzlThreeGridChild *child = dzl_three_grid_find_child (self, widget); + + switch (prop_id) + { + case CHILD_PROP_COLUMN: + g_value_set_enum (value, child->column); + break; + + case CHILD_PROP_ROW: + g_value_set_uint (value, child->row); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } +} + +static void +dzl_three_grid_set_child_property (GtkContainer *container, + GtkWidget *widget, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlThreeGrid *self = (DzlThreeGrid *)container; + DzlThreeGridChild *child = dzl_three_grid_find_child (self, widget); + + switch (prop_id) + { + case CHILD_PROP_COLUMN: + child->column = g_value_get_enum (value); + break; + + case CHILD_PROP_ROW: + child->row = g_value_get_uint (value); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); + } + + gtk_widget_queue_resize (GTK_WIDGET (container)); +} + +static void +dzl_three_grid_finalize (GObject *object) +{ + DzlThreeGrid *self = (DzlThreeGrid *)object; + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + + g_clear_pointer (&priv->row_infos, g_hash_table_unref); + g_clear_pointer (&priv->children, g_ptr_array_unref); + + G_OBJECT_CLASS (dzl_three_grid_parent_class)->finalize (object); +} + +static void +dzl_three_grid_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + DzlThreeGrid *self = DZL_THREE_GRID (object); + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + + switch (prop_id) + { + case PROP_COLUMN_SPACING: + g_value_set_uint (value, priv->column_spacing); + break; + + case PROP_ROW_SPACING: + g_value_set_uint (value, priv->row_spacing); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_three_grid_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + DzlThreeGrid *self = DZL_THREE_GRID (object); + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + + switch (prop_id) + { + case PROP_COLUMN_SPACING: + priv->column_spacing = g_value_get_uint (value); + gtk_widget_queue_resize (GTK_WIDGET (self)); + break; + + case PROP_ROW_SPACING: + priv->row_spacing = g_value_get_uint (value); + gtk_widget_queue_resize (GTK_WIDGET (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +dzl_three_grid_class_init (DzlThreeGridClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->finalize = dzl_three_grid_finalize; + object_class->get_property = dzl_three_grid_get_property; + object_class->set_property = dzl_three_grid_set_property; + + widget_class->get_request_mode = dzl_three_grid_get_request_mode; + widget_class->get_preferred_height_for_width = dzl_three_grid_get_preferred_height_for_width; + widget_class->get_preferred_width = dzl_three_grid_get_preferred_width; + widget_class->size_allocate = dzl_three_grid_size_allocate; + + container_class->add = dzl_three_grid_add; + container_class->forall = dzl_three_grid_forall; + container_class->get_child_property = dzl_three_grid_get_child_property; + container_class->remove = dzl_three_grid_remove; + container_class->set_child_property = dzl_three_grid_set_child_property; + + properties [PROP_COLUMN_SPACING] = + g_param_spec_uint ("column-spacing", + "Column Spacing", + "The amount of spacing to add between columns", + 0, + G_MAXUINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties [PROP_ROW_SPACING] = + g_param_spec_uint ("row-spacing", + "Row Spacing", + "The amount of spacing to add between rows", + 0, + G_MAXUINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + child_properties [CHILD_PROP_COLUMN] = + g_param_spec_enum ("column", + "Column", + "Column", + DZL_TYPE_THREE_GRID_COLUMN, + DZL_THREE_GRID_COLUMN_LEFT, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + child_properties [CHILD_PROP_ROW] = + g_param_spec_uint ("row", + "Row", + "Row", + 0, + G_MAXUINT, + 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + gtk_container_class_install_child_properties (container_class, N_CHILD_PROPS, child_properties); + + gtk_widget_class_set_css_name (widget_class, "threegrid"); +} + +static void +dzl_three_grid_init (DzlThreeGrid *self) +{ + DzlThreeGridPrivate *priv = dzl_three_grid_get_instance_private (self); + + priv->children = g_ptr_array_new_with_free_func (dzl_three_grid_child_free); + + gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); +} + +GtkWidget * +dzl_three_grid_new (void) +{ + return g_object_new (DZL_TYPE_THREE_GRID, NULL); +} + +GType +dzl_three_grid_column_get_type (void) +{ + static GType type_id; + + if (g_once_init_enter (&type_id)) + { + GType _type_id; + static const GEnumValue values[] = { + { DZL_THREE_GRID_COLUMN_LEFT, "DZL_THREE_GRID_COLUMN_LEFT", "left" }, + { DZL_THREE_GRID_COLUMN_CENTER, "DZL_THREE_GRID_COLUMN_CENTER", "center" }, + { DZL_THREE_GRID_COLUMN_RIGHT, "DZL_THREE_GRID_COLUMN_RIGHT", "right" }, + { 0 } + }; + _type_id = g_enum_register_static ("DzlThreeGridColumn", values); + g_once_init_leave (&type_id, _type_id); + } + + return type_id; +} diff --git a/src/widgets/dzl-three-grid.h b/src/widgets/dzl-three-grid.h new file mode 100644 index 0000000..9f71559 --- /dev/null +++ b/src/widgets/dzl-three-grid.h @@ -0,0 +1,62 @@ +/* dzl-three-grid.h + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#ifndef DZL_THREE_GRID_H +#define DZL_THREE_GRID_H + +#include + +#include "dzl-version-macros.h" + +G_BEGIN_DECLS + +#define DZL_TYPE_THREE_GRID (dzl_three_grid_get_type()) +#define DZL_TYPE_THREE_GRID_COLUMN (dzl_three_grid_column_get_type()) + +DZL_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (DzlThreeGrid, dzl_three_grid, DZL, THREE_GRID, GtkContainer) + +struct _DzlThreeGridClass +{ + GtkContainerClass parent_class; + + gpointer _reserved1; + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + gpointer _reserved8; +}; + +typedef enum +{ + DZL_THREE_GRID_COLUMN_LEFT, + DZL_THREE_GRID_COLUMN_CENTER, + DZL_THREE_GRID_COLUMN_RIGHT +} DzlThreeGridColumn; + +DZL_AVAILABLE_IN_ALL +GType dzl_three_grid_column_get_type (void); +DZL_AVAILABLE_IN_ALL +GtkWidget *dzl_three_grid_new (void); + +G_END_DECLS + +#endif /* DZL_THREE_GRID_H */ diff --git a/src/widgets/meson.build b/src/widgets/meson.build new file mode 100644 index 0000000..cbb0fa3 --- /dev/null +++ b/src/widgets/meson.build @@ -0,0 +1,66 @@ +widgets_headers = [ + 'dzl-bin.h', + 'dzl-bolding-label.h', + 'dzl-box.h', + 'dzl-centering-bin.h', + 'dzl-column-layout.h', + 'dzl-elastic-bin.h', + 'dzl-empty-state.h', + 'dzl-entry-box.h', + 'dzl-file-chooser-entry.h', + 'dzl-list-box.h', + 'dzl-list-box-row.h', + 'dzl-multi-paned.h', + 'dzl-pill-box.h', + 'dzl-priority-box.h', + 'dzl-progress-button.h', + 'dzl-progress-menu-button.h', + 'dzl-progress-icon.h', + 'dzl-radio-box.h', + 'dzl-scrolled-window.h', + 'dzl-search-bar.h', + 'dzl-simple-label.h', + 'dzl-simple-popover.h', + 'dzl-slider.h', + 'dzl-stack-list.h', + 'dzl-three-grid.h', +] + +widgets_sources = [ + 'dzl-bin.c', + 'dzl-bolding-label.c', + 'dzl-box.c', + 'dzl-centering-bin.c', + 'dzl-column-layout.c', + 'dzl-elastic-bin.c', + 'dzl-empty-state.c', + 'dzl-entry-box.c', + 'dzl-file-chooser-entry.c', + 'dzl-list-box.c', + 'dzl-list-box-row.c', + 'dzl-multi-paned.c', + 'dzl-pill-box.c', + 'dzl-priority-box.c', + 'dzl-progress-button.c', + 'dzl-progress-menu-button.c', + 'dzl-progress-icon.c', + 'dzl-radio-box.c', + 'dzl-scrolled-window.c', + 'dzl-search-bar.c', + 'dzl-simple-label.c', + 'dzl-simple-popover.c', + 'dzl-slider.c', + 'dzl-stack-list.c', + 'dzl-three-grid.c', +] + +if host_machine.system() != 'windows' + widgets_headers += ['dzl-counters-window.h'] + widgets_sources += ['dzl-counters-window.c'] +endif + +libdazzle_public_headers += files(widgets_headers) +libdazzle_public_sources += files(widgets_sources) +libdazzle_private_sources += files('dzl-rect-helper.c') + +install_headers(widgets_headers, subdir: join_paths(libdazzle_header_subdir, 'widgets')) diff --git a/tests/data/keythemes/default.keytheme b/tests/data/keythemes/default.keytheme new file mode 100644 index 0000000..d564426 --- /dev/null +++ b/tests/data/keythemes/default.keytheme @@ -0,0 +1,6 @@ + + + Default + The default theme + + diff --git a/tests/data/keythemes/emacs.keytheme b/tests/data/keythemes/emacs.keytheme new file mode 100644 index 0000000..a3c6203 --- /dev/null +++ b/tests/data/keythemes/emacs.keytheme @@ -0,0 +1,5 @@ + + + Emacs + Emacs theme + diff --git a/tests/data/keythemes/test.keytheme b/tests/data/keythemes/test.keytheme new file mode 100644 index 0000000..09d758e --- /dev/null +++ b/tests/data/keythemes/test.keytheme @@ -0,0 +1,24 @@ + + + Test + Test theme + + + + + + + + + + + 0 + + + + + + + + + diff --git a/tests/data/keythemes/vim.keytheme b/tests/data/keythemes/vim.keytheme new file mode 100644 index 0000000..8b9f2f7 --- /dev/null +++ b/tests/data/keythemes/vim.keytheme @@ -0,0 +1,5 @@ + + + Vim + Vim theme + diff --git a/tests/data/menus/joined1.ui b/tests/data/menus/joined1.ui new file mode 100644 index 0000000..c5bf5a4 --- /dev/null +++ b/tests/data/menus/joined1.ui @@ -0,0 +1,46 @@ + + + +
+ Document + + editor-document-open-in-new-frame + Open in New Frame + layoutstack.open-in-new-frame + document-open-symbolic + + + Split + layoutstack.split-view + + + Print… + editor-view.print + +
+
+ editor-document-section + + Document Preferences + +
+
+ editor-document-preferences-section + + editor-view.save + _Save + + + editor-view.save-as + Save _As + +
+
+ editor-document-save-section + + layoutstack.close-view + Close + +
+
+
diff --git a/tests/data/menus/joined2.ui b/tests/data/menus/joined2.ui new file mode 100644 index 0000000..4f49bac --- /dev/null +++ b/tests/data/menus/joined2.ui @@ -0,0 +1,20 @@ + + + +
+ Frame + + Move Left + layoutstack.move-left + + + Move Right + layoutstack.move-right + + + layoutgrid.close-stack + Close + +
+
+
diff --git a/tests/data/menus/menus-exten-1.ui b/tests/data/menus/menus-exten-1.ui new file mode 100644 index 0000000..43f257b --- /dev/null +++ b/tests/data/menus/menus-exten-1.ui @@ -0,0 +1,17 @@ + + + + + + diff --git a/tests/data/menus/menus-exten-2.ui b/tests/data/menus/menus-exten-2.ui new file mode 100644 index 0000000..5b7cfbb --- /dev/null +++ b/tests/data/menus/menus-exten-2.ui @@ -0,0 +1,12 @@ + + + + + + diff --git a/tests/data/menus/menus-exten-3.ui b/tests/data/menus/menus-exten-3.ui new file mode 100644 index 0000000..17a301e --- /dev/null +++ b/tests/data/menus/menus-exten-3.ui @@ -0,0 +1,20 @@ + + + + + + diff --git a/tests/data/menus/menus-exten-4.ui b/tests/data/menus/menus-exten-4.ui new file mode 100644 index 0000000..387cd90 --- /dev/null +++ b/tests/data/menus/menus-exten-4.ui @@ -0,0 +1,11 @@ + + + + + + diff --git a/tests/data/menus/menus-exten-5.ui b/tests/data/menus/menus-exten-5.ui new file mode 100644 index 0000000..1fbbc4a --- /dev/null +++ b/tests/data/menus/menus-exten-5.ui @@ -0,0 +1,16 @@ + + + + + + diff --git a/tests/data/menus/menus.ui b/tests/data/menus/menus.ui new file mode 100644 index 0000000..93de611 --- /dev/null +++ b/tests/data/menus/menus.ui @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + diff --git a/tests/data/shortcuts/0/gtk/menus.ui b/tests/data/shortcuts/0/gtk/menus.ui new file mode 100644 index 0000000..2aa8159 --- /dev/null +++ b/tests/data/shortcuts/0/gtk/menus.ui @@ -0,0 +1,9 @@ + + + +
+
+
+
+
+
diff --git a/tests/data/shortcuts/0/shortcuts/default.keytheme b/tests/data/shortcuts/0/shortcuts/default.keytheme new file mode 100644 index 0000000..d564426 --- /dev/null +++ b/tests/data/shortcuts/0/shortcuts/default.keytheme @@ -0,0 +1,6 @@ + + + Default + The default theme + + diff --git a/tests/data/shortcuts/0/shortcuts/secondary.keytheme b/tests/data/shortcuts/0/shortcuts/secondary.keytheme new file mode 100644 index 0000000..93bb2d4 --- /dev/null +++ b/tests/data/shortcuts/0/shortcuts/secondary.keytheme @@ -0,0 +1,6 @@ + + + Secondary + + + diff --git a/tests/data/shortcuts/1/gtk/menus.ui b/tests/data/shortcuts/1/gtk/menus.ui new file mode 100644 index 0000000..6c69f3a --- /dev/null +++ b/tests/data/shortcuts/1/gtk/menus.ui @@ -0,0 +1,19 @@ + + + +
+ + section-2-item-1 + Item 2.1 + dummy + +
+
+ + section-4-item-1 + Item 4.1 + dummy + +
+
+
diff --git a/tests/data/shortcuts/2/gtk/menus.ui b/tests/data/shortcuts/2/gtk/menus.ui new file mode 100644 index 0000000..ed9cc46 --- /dev/null +++ b/tests/data/shortcuts/2/gtk/menus.ui @@ -0,0 +1,13 @@ + + + +
+ + section-2-item-2 + section-2-item-1 + Item 2.2 + dummy + +
+
+
diff --git a/tests/data/test-fuzzy-mutable-index.txt b/tests/data/test-fuzzy-mutable-index.txt new file mode 100644 index 0000000..56bb75a --- /dev/null +++ b/tests/data/test-fuzzy-mutable-index.txt @@ -0,0 +1,16593 @@ +acquire_bitmap +acquire_screen +acs_map +active_dialog +active_menu +add_clip_rect +add_wch +add_wchnstr +add_wchstr +adjust_sample +aio_cancel +aio_error +aio_fsync +aio_init +aio_read +aio_return +aio_suspend +aio_write +al_assert +al_ffblk +al_ffblk_get_size +al_findclose +al_findfirst +al_findnext +AL_ID +aligned_alloc +allegro_404_char +ALLEGRO_DATE +ALLEGRO_DATE_STR +allegro_error +allegro_exit +allegro_icon +allegro_id +allegro_init +allegro_message +ALLEGRO_SUB_VERSION +ALLEGRO_VERSION +ALLEGRO_VERSION_STR +ALLEGRO_WIP_VERSION +allocate_voice +al_trace +AnyDBM_File +append_filename +apply_matrix +apply_matrix_f +apply_quat +argz_add +argz_add_sep +argz_append +argz_count +argz_create +argz_create_sep +argz_delete +argz_extract +argz_insert +argz_next +argz_replace +argz_stringify +asctime_r +asn1_array2tree +asn1_bit_der +asn1_check_version +asn1_copy_node +asn1_create_element +asn1_delete_element +asn1_deleteure +asn1_der_coding +asn1_der_decoding +asn1_der_decoding_element +asn1_der_decoding_startEnd +asn1_expand_any_defined_by +asn1_expand_octet_string +asn1_find_node +asn1_findure_from_oid +asn1_get_bit_der +asn1_get_length_ber +asn1_get_length_der +asn1_get_octet_der +asn1_get_tag_der +asn1_length_der +asn1_number_of_elements +asn1_octet_der +asn1_parser2array +asn1_parser2tree +asn1_perror +asn1_printure +asn1_read_node_value +asn1_read_tag +asn1_read_value +asn1_strerror +asn1_write_value +assert_perror +assume_default_colors +assume_default_colors_sp +attr_get +attr_off +attr_on +attr_set +auth_destroy +authnone_create +authunix_create +authunix_create_default +backtrace_symbols +backtrace_symbols_fd +baudrate_sp +beep_sp +bestfit_color +bind_textdomain_codeset +bitmap_color_depth +bitmap_mask_color +black_palette +BLOB_IS_REGISTERED_TYPE +bmp_read_line +bmp_unwrite_line +bmp_write_line +border_set +bottom_panel +box_set +broadcast_dialog_message +bsd_signal +C_ +calc_spline +calibrate_joystick +calibrate_joystick_name +can_change_color +can_change_color_sp +canonicalize_filename +canonicalize_file_name +cbc_crypt +cbreak_sp +ceiling_panel +centre_dialog +check_cpu +CIRCLEQ_ENTRY +CIRCLEQ_HEAD +CIRCLEQ_INIT +CIRCLEQ_INSERT_AFTER +CIRCLEQ_INSERT_BEFORE +CIRCLEQ_INSERT_HEAD +CIRCLEQ_INSERT_TAIL +CIRCLEQ_REMOVE +clear_bitmap +clearerr_unlocked +clear_keybuf +clear_scene +clear_to_color +clear_zbuffer +clip3d_f +clnt_broadcast +clnt_call +clnt_control +clnt_create +clnt_destroy +clnt_freeres +clnt_geterr +clnt_pcreateerror +clnt_perrno +clnt_perror +clntraw_create +clnt_spcreateerror +clnt_sperrno +clnt_sperror +clnttcp_create +clntudp_bufcreate +clntudp_create +clock_getcpuclockid +clock_getres +clock_gettime +clock_settime +close_fli +clutter_actor_add_action +clutter_actor_add_action_with_name +clutter_actor_add_child +clutter_actor_add_constraint +clutter_actor_add_constraint_with_name +clutter_actor_add_effect +clutter_actor_add_effect_with_name +clutter_actor_add_transition +clutter_actor_allocate +clutter_actor_allocate_align_fill +clutter_actor_allocate_available_size +clutter_actor_allocate_preferred_size +clutter_actor_animate +clutter_actor_animatev +clutter_actor_animate_with_alpha +clutter_actor_animate_with_alphav +clutter_actor_animate_with_timeline +clutter_actor_animate_with_timelinev +clutter_actor_apply_relative_transform_to_point +clutter_actor_apply_transform_to_point +clutter_actor_box_alloc +clutter_actor_box_clamp_to_pixel +clutter_actor_box_contains +clutter_actor_box_copy +clutter_actor_box_equal +clutter_actor_box_free +clutter_actor_box_from_vertices +clutter_actor_box_get_area +clutter_actor_box_get_height +clutter_actor_box_get_origin +clutter_actor_box_get_size +clutter_actor_box_get_width +clutter_actor_box_get_x +clutter_actor_box_get_y +clutter_actor_box_init +CLUTTER_ACTOR_BOX_INIT +clutter_actor_box_init_rect +CLUTTER_ACTOR_BOX_INIT_ZERO +clutter_actor_box_interpolate +clutter_actor_box_new +clutter_actor_box_set_origin +clutter_actor_box_set_size +clutter_actor_box_union +clutter_actor_clear_actions +clutter_actor_clear_constraints +clutter_actor_clear_effects +clutter_actor_contains +clutter_actor_continue_paint +clutter_actor_create_pango_context +clutter_actor_create_pango_layout +clutter_actor_destroy +clutter_actor_destroy_all_children +clutter_actor_detach_animation +clutter_actor_event +clutter_actor_get_abs_allocation_vertices +clutter_actor_get_accessible +clutter_actor_get_action +clutter_actor_get_actions +clutter_actor_get_allocation_box +clutter_actor_get_allocation_geometry +clutter_actor_get_allocation_vertices +clutter_actor_get_anchor_point +clutter_actor_get_anchor_point_gravity +clutter_actor_get_animation +clutter_actor_get_background_color +clutter_actor_get_child_at_index +clutter_actor_get_children +clutter_actor_get_child_transform +clutter_actor_get_clip +clutter_actor_get_clip_to_allocation +clutter_actor_get_constraint +clutter_actor_get_constraints +clutter_actor_get_content +clutter_actor_get_content_box +clutter_actor_get_content_gravity +clutter_actor_get_content_repeat +clutter_actor_get_content_scaling_filters +clutter_actor_get_default_paint_volume +clutter_actor_get_depth +clutter_actor_get_easing_delay +clutter_actor_get_easing_duration +clutter_actor_get_easing_mode +clutter_actor_get_effect +clutter_actor_get_effects +clutter_actor_get_first_child +clutter_actor_get_fixed_position_set +clutter_actor_get_flags +clutter_actor_get_geometry +clutter_actor_get_gid +clutter_actor_get_height +clutter_actor_get_last_child +clutter_actor_get_layout_manager +clutter_actor_get_margin +clutter_actor_get_margin_bottom +clutter_actor_get_margin_left +clutter_actor_get_margin_right +clutter_actor_get_margin_top +clutter_actor_get_name +clutter_actor_get_n_children +clutter_actor_get_next_sibling +clutter_actor_get_offscreen_redirect +clutter_actor_get_opacity +clutter_actor_get_paint_box +clutter_actor_get_paint_opacity +clutter_actor_get_paint_visibility +clutter_actor_get_paint_volume +clutter_actor_get_pango_context +clutter_actor_get_parent +clutter_actor_get_pivot_point +clutter_actor_get_pivot_point_z +clutter_actor_get_position +clutter_actor_get_preferred_height +clutter_actor_get_preferred_size +clutter_actor_get_preferred_width +clutter_actor_get_previous_sibling +clutter_actor_get_reactive +clutter_actor_get_request_mode +clutter_actor_get_rotation +clutter_actor_get_rotation_angle +clutter_actor_get_scale +clutter_actor_get_scale_center +clutter_actor_get_scale_gravity +clutter_actor_get_scale_z +clutter_actor_get_shader +clutter_actor_get_size +clutter_actor_get_stage +clutter_actor_get_text_direction +clutter_actor_get_transform +clutter_actor_get_transformation_matrix +clutter_actor_get_transformed_paint_volume +clutter_actor_get_transformed_position +clutter_actor_get_transformed_size +clutter_actor_get_transition +clutter_actor_get_translation +clutter_actor_get_width +clutter_actor_get_x +clutter_actor_get_x_align +clutter_actor_get_x_expand +clutter_actor_get_y +clutter_actor_get_y_align +clutter_actor_get_y_expand +clutter_actor_get_z_position +clutter_actor_get_z_rotation_gravity +clutter_actor_grab_key_focus +clutter_actor_has_actions +clutter_actor_has_allocation +clutter_actor_has_clip +clutter_actor_has_constraints +clutter_actor_has_effects +clutter_actor_has_key_focus +clutter_actor_has_overlaps +clutter_actor_has_pointer +clutter_actor_hide +clutter_actor_hide_all +clutter_actor_insert_child_above +clutter_actor_insert_child_at_index +clutter_actor_insert_child_below +clutter_actor_is_in_clone_paint +CLUTTER_ACTOR_IS_MAPPED +CLUTTER_ACTOR_IS_REACTIVE +CLUTTER_ACTOR_IS_REALIZED +clutter_actor_is_rotated +clutter_actor_is_scaled +CLUTTER_ACTOR_IS_VISIBLE +clutter_actor_iter_destroy +clutter_actor_iter_init +clutter_actor_iter_is_valid +clutter_actor_iter_next +clutter_actor_iter_prev +clutter_actor_iter_remove +clutter_actor_lower +clutter_actor_lower_bottom +clutter_actor_map +clutter_actor_meta_get_actor +clutter_actor_meta_get_enabled +clutter_actor_meta_get_name +clutter_actor_meta_set_enabled +clutter_actor_meta_set_name +clutter_actor_move_anchor_point +clutter_actor_move_anchor_point_from_gravity +clutter_actor_move_by +clutter_actor_needs_expand +clutter_actor_new +clutter_actor_paint +clutter_actor_pop_internal +clutter_actor_push_internal +clutter_actor_queue_redraw +clutter_actor_queue_redraw_with_clip +clutter_actor_queue_relayout +clutter_actor_raise +clutter_actor_raise_top +clutter_actor_realize +clutter_actor_remove_action +clutter_actor_remove_action_by_name +clutter_actor_remove_all_children +clutter_actor_remove_all_transitions +clutter_actor_remove_child +clutter_actor_remove_clip +clutter_actor_remove_constraint +clutter_actor_remove_constraint_by_name +clutter_actor_remove_effect +clutter_actor_remove_effect_by_name +clutter_actor_remove_transition +clutter_actor_reparent +clutter_actor_replace_child +clutter_actor_restore_easing_state +clutter_actor_save_easing_state +clutter_actor_set_allocation +clutter_actor_set_anchor_point +clutter_actor_set_anchor_point_from_gravity +clutter_actor_set_background_color +clutter_actor_set_child_above_sibling +clutter_actor_set_child_at_index +clutter_actor_set_child_below_sibling +clutter_actor_set_child_transform +clutter_actor_set_clip +clutter_actor_set_clip_to_allocation +clutter_actor_set_content +clutter_actor_set_content_gravity +clutter_actor_set_content_repeat +clutter_actor_set_content_scaling_filters +clutter_actor_set_depth +clutter_actor_set_easing_delay +clutter_actor_set_easing_duration +clutter_actor_set_easing_mode +clutter_actor_set_fixed_position_set +clutter_actor_set_flags +CLUTTER_ACTOR_SET_FLAGS +clutter_actor_set_geometry +clutter_actor_set_height +clutter_actor_set_layout_manager +clutter_actor_set_margin +clutter_actor_set_margin_bottom +clutter_actor_set_margin_left +clutter_actor_set_margin_right +clutter_actor_set_margin_top +clutter_actor_set_name +clutter_actor_set_offscreen_redirect +clutter_actor_set_opacity +clutter_actor_set_parent +clutter_actor_set_pivot_point +clutter_actor_set_pivot_point_z +clutter_actor_set_position +clutter_actor_set_reactive +clutter_actor_set_request_mode +clutter_actor_set_rotation +clutter_actor_set_rotation_angle +clutter_actor_set_scale +clutter_actor_set_scale_full +clutter_actor_set_scale_with_gravity +clutter_actor_set_scale_z +clutter_actor_set_shader +clutter_actor_set_shader_param +clutter_actor_set_shader_param_float +clutter_actor_set_shader_param_int +clutter_actor_set_size +clutter_actor_set_text_direction +clutter_actor_set_transform +clutter_actor_set_translation +clutter_actor_set_width +clutter_actor_set_x +clutter_actor_set_x_align +clutter_actor_set_x_expand +clutter_actor_set_y +clutter_actor_set_y_align +clutter_actor_set_y_expand +clutter_actor_set_z_position +clutter_actor_set_z_rotation_from_gravity +clutter_actor_should_pick_paint +clutter_actor_show +clutter_actor_show_all +clutter_actor_transform_stage_point +clutter_actor_unmap +clutter_actor_unparent +clutter_actor_unrealize +clutter_actor_unset_flags +CLUTTER_ACTOR_UNSET_FLAGS +clutter_align_constraint_get_align_axis +clutter_align_constraint_get_factor +clutter_align_constraint_get_source +clutter_align_constraint_new +clutter_align_constraint_set_align_axis +clutter_align_constraint_set_factor +clutter_align_constraint_set_source +clutter_alpha_get_alpha +clutter_alpha_get_mode +clutter_alpha_get_timeline +clutter_alpha_new +clutter_alpha_new_full +clutter_alpha_new_with_func +clutter_alpha_register_closure +clutter_alpha_register_func +clutter_alpha_set_closure +clutter_alpha_set_func +clutter_alpha_set_mode +clutter_alpha_set_timeline +clutter_animatable_animate_property +clutter_animatable_find_property +clutter_animatable_get_initial_state +clutter_animatable_interpolate_value +clutter_animatable_set_final_state +clutter_animation_bind +clutter_animation_bind_interval +clutter_animation_completed +clutter_animation_get_alpha +clutter_animation_get_duration +clutter_animation_get_interval +clutter_animation_get_loop +clutter_animation_get_mode +clutter_animation_get_object +clutter_animation_get_timeline +clutter_animation_has_property +clutter_animation_new +clutter_animation_set_alpha +clutter_animation_set_duration +clutter_animation_set_loop +clutter_animation_set_mode +clutter_animation_set_object +clutter_animation_set_timeline +clutter_animation_unbind_property +clutter_animation_update +clutter_animation_update_interval +clutter_animator_compute_value +clutter_animator_get_duration +clutter_animator_get_keys +clutter_animator_get_timeline +clutter_animator_key_get_mode +clutter_animator_key_get_object +clutter_animator_key_get_progress +clutter_animator_key_get_property_name +clutter_animator_key_get_property_type +clutter_animator_key_get_value +clutter_animator_new +clutter_animator_property_get_ease_in +clutter_animator_property_get_interpolation +clutter_animator_property_set_ease_in +clutter_animator_property_set_interpolation +clutter_animator_remove_key +clutter_animator_set +clutter_animator_set_duration +clutter_animator_set_key +clutter_animator_set_timeline +clutter_animator_start +clutter_backend_get_cogl_context +clutter_backend_get_double_click_distance +clutter_backend_get_double_click_time +clutter_backend_get_font_name +clutter_backend_get_font_options +clutter_backend_get_resolution +clutter_backend_set_double_click_distance +clutter_backend_set_double_click_time +clutter_backend_set_font_name +clutter_backend_set_font_options +clutter_backend_set_resolution +clutter_behaviour_actors_foreach +clutter_behaviour_apply +clutter_behaviour_depth_get_bounds +clutter_behaviour_depth_new +clutter_behaviour_depth_set_bounds +clutter_behaviour_ellipse_get_angle_end +clutter_behaviour_ellipse_get_angle_start +clutter_behaviour_ellipse_get_angle_tilt +clutter_behaviour_ellipse_get_center +clutter_behaviour_ellipse_get_direction +clutter_behaviour_ellipse_get_height +clutter_behaviour_ellipse_get_tilt +clutter_behaviour_ellipse_get_width +clutter_behaviour_ellipse_new +clutter_behaviour_ellipse_set_angle_end +clutter_behaviour_ellipse_set_angle_start +clutter_behaviour_ellipse_set_angle_tilt +clutter_behaviour_ellipse_set_center +clutter_behaviour_ellipse_set_direction +clutter_behaviour_ellipse_set_height +clutter_behaviour_ellipse_set_tilt +clutter_behaviour_ellipse_set_width +clutter_behaviour_get_actors +clutter_behaviour_get_alpha +clutter_behaviour_get_n_actors +clutter_behaviour_get_nth_actor +clutter_behaviour_is_applied +clutter_behaviour_opacity_get_bounds +clutter_behaviour_opacity_new +clutter_behaviour_opacity_set_bounds +clutter_behaviour_path_get_path +clutter_behaviour_path_new +clutter_behaviour_path_new_with_description +clutter_behaviour_path_new_with_knots +clutter_behaviour_path_set_path +clutter_behaviour_remove +clutter_behaviour_remove_all +clutter_behaviour_rotate_get_axis +clutter_behaviour_rotate_get_bounds +clutter_behaviour_rotate_get_center +clutter_behaviour_rotate_get_direction +clutter_behaviour_rotate_new +clutter_behaviour_rotate_set_axis +clutter_behaviour_rotate_set_bounds +clutter_behaviour_rotate_set_center +clutter_behaviour_rotate_set_direction +clutter_behaviour_scale_get_bounds +clutter_behaviour_scale_new +clutter_behaviour_scale_set_bounds +clutter_behaviour_set_alpha +clutter_bind_constraint_get_coordinate +clutter_bind_constraint_get_offset +clutter_bind_constraint_get_source +clutter_bind_constraint_new +clutter_bind_constraint_set_coordinate +clutter_bind_constraint_set_offset +clutter_bind_constraint_set_source +clutter_binding_pool_activate +clutter_binding_pool_block_action +clutter_binding_pool_find +clutter_binding_pool_find_action +clutter_binding_pool_get_for_class +clutter_binding_pool_install_action +clutter_binding_pool_install_closure +clutter_binding_pool_new +clutter_binding_pool_override_action +clutter_binding_pool_override_closure +clutter_binding_pool_remove_action +clutter_binding_pool_unblock_action +clutter_bin_layout_add +clutter_bin_layout_get_alignment +clutter_bin_layout_new +clutter_bin_layout_set_alignment +clutter_blur_effect_new +clutter_box_get_color +clutter_box_get_layout_manager +clutter_box_layout_get_alignment +clutter_box_layout_get_easing_duration +clutter_box_layout_get_easing_mode +clutter_box_layout_get_expand +clutter_box_layout_get_fill +clutter_box_layout_get_homogeneous +clutter_box_layout_get_orientation +clutter_box_layout_get_pack_start +clutter_box_layout_get_spacing +clutter_box_layout_get_use_animations +clutter_box_layout_get_vertical +clutter_box_layout_new +clutter_box_layout_pack +clutter_box_layout_set_alignment +clutter_box_layout_set_easing_duration +clutter_box_layout_set_easing_mode +clutter_box_layout_set_expand +clutter_box_layout_set_fill +clutter_box_layout_set_homogeneous +clutter_box_layout_set_orientation +clutter_box_layout_set_pack_start +clutter_box_layout_set_spacing +clutter_box_layout_set_use_animations +clutter_box_layout_set_vertical +clutter_box_new +clutter_box_pack +clutter_box_pack_after +clutter_box_pack_at +clutter_box_pack_before +clutter_box_packv +clutter_box_set_color +clutter_box_set_layout_manager +clutter_brightness_contrast_effect_get_brightness +clutter_brightness_contrast_effect_get_contrast +clutter_brightness_contrast_effect_new +clutter_brightness_contrast_effect_set_brightness +clutter_brightness_contrast_effect_set_brightness_full +clutter_brightness_contrast_effect_set_contrast +clutter_brightness_contrast_effect_set_contrast_full +CLUTTER_BUTTON_MIDDLE +CLUTTER_BUTTON_PRIMARY +CLUTTER_BUTTON_SECONDARY +clutter_cairo_clear +CLUTTER_CAIRO_FORMAT_ARGB32 +clutter_cairo_set_source_color +clutter_cairo_texture_clear +clutter_cairo_texture_create +clutter_cairo_texture_create_region +clutter_cairo_texture_get_auto_resize +clutter_cairo_texture_get_surface_size +clutter_cairo_texture_invalidate +clutter_cairo_texture_invalidate_rectangle +clutter_cairo_texture_new +clutter_cairo_texture_set_auto_resize +clutter_cairo_texture_set_surface_size +CLUTTER_CALLBACK +clutter_canvas_new +clutter_canvas_set_size +clutter_cex100_get_egl_display +clutter_cex100_set_buffering_mode +clutter_cex100_set_plane +clutter_check_version +CLUTTER_CHECK_VERSION +clutter_check_windowing_backend +clutter_child_meta_get_actor +clutter_child_meta_get_container +clutter_clear_glyph_cache +clutter_click_action_get_button +clutter_click_action_get_coords +clutter_click_action_get_state +clutter_click_action_new +clutter_click_action_release +clutter_clip_node_new +clutter_clone_get_source +clutter_clone_new +clutter_clone_set_source +CLUTTER_COGL +clutter_color_add +clutter_color_alloc +clutter_color_copy +clutter_color_darken +clutter_color_equal +clutter_color_free +clutter_color_from_hls +clutter_color_from_pixel +clutter_color_from_string +clutter_color_get_static +clutter_color_hash +clutter_color_init +CLUTTER_COLOR_INIT +clutter_color_interpolate +clutter_colorize_effect_get_tint +clutter_colorize_effect_new +clutter_colorize_effect_set_tint +clutter_color_lighten +clutter_color_new +clutter_color_node_new +clutter_color_shade +clutter_color_subtract +clutter_color_to_hls +clutter_color_to_pixel +clutter_color_to_string +clutter_container_add +clutter_container_add_actor +clutter_container_add_valist +clutter_container_child_get +clutter_container_child_get_property +clutter_container_child_notify +clutter_container_child_set +clutter_container_child_set_property +clutter_container_class_find_child_property +clutter_container_class_list_child_properties +clutter_container_create_child_meta +clutter_container_destroy_child_meta +clutter_container_find_child_by_name +clutter_container_foreach +clutter_container_foreach_with_internals +clutter_container_get_child_meta +clutter_container_get_children +clutter_container_lower_child +clutter_container_raise_child +clutter_container_remove +clutter_container_remove_actor +clutter_container_remove_valist +clutter_container_sort_depth_order +clutter_content_get_preferred_size +clutter_content_invalidate +CLUTTER_CURRENT_TIME +clutter_deform_effect_get_back_material +clutter_deform_effect_get_n_tiles +clutter_deform_effect_invalidate +clutter_deform_effect_set_back_material +clutter_deform_effect_set_n_tiles +clutter_desaturate_effect_get_factor +clutter_desaturate_effect_new +clutter_desaturate_effect_set_factor +clutter_device_manager_get_core_device +clutter_device_manager_get_default +clutter_device_manager_get_device +clutter_device_manager_list_devices +clutter_device_manager_peek_devices +clutter_do_event +clutter_drag_action_get_drag_area +clutter_drag_action_get_drag_axis +clutter_drag_action_get_drag_handle +clutter_drag_action_get_drag_threshold +clutter_drag_action_get_motion_coords +clutter_drag_action_get_press_coords +clutter_drag_action_new +clutter_drag_action_set_drag_area +clutter_drag_action_set_drag_axis +clutter_drag_action_set_drag_handle +clutter_drag_action_set_drag_threshold +clutter_drop_action_new +clutter_effect_queue_repaint +clutter_egl_display +clutter_egl_get_egl_display +clutter_eglx_display +clutter_event_copy +clutter_event_free +clutter_event_get +clutter_event_get_angle +clutter_event_get_axes +clutter_event_get_button +clutter_event_get_click_count +clutter_event_get_coords +clutter_event_get_device +clutter_event_get_device_id +clutter_event_get_device_type +clutter_event_get_distance +clutter_event_get_event_sequence +clutter_event_get_flags +clutter_event_get_key_code +clutter_event_get_key_symbol +clutter_event_get_key_unicode +clutter_event_get_position +clutter_event_get_related +clutter_event_get_scroll_delta +clutter_event_get_scroll_direction +clutter_event_get_source +clutter_event_get_source_device +clutter_event_get_stage +clutter_event_get_state +clutter_event_get_time +clutter_event_has_control_modifier +clutter_event_has_shift_modifier +clutter_event_is_pointer_emulated +clutter_event_new +clutter_event_peek +CLUTTER_EVENT_PROPAGATE +clutter_event_put +clutter_event_set_button +clutter_event_set_coords +clutter_event_set_device +clutter_event_set_flags +clutter_event_set_key_code +clutter_event_set_key_symbol +clutter_event_set_key_unicode +clutter_event_set_related +clutter_event_set_scroll_delta +clutter_event_set_scroll_direction +clutter_event_set_source +clutter_event_set_source_device +clutter_event_set_stage +clutter_event_set_state +clutter_event_set_time +clutter_events_pending +CLUTTER_EVENT_STOP +clutter_event_type +clutter_feature_available +clutter_feature_get_all +clutter_fixed_layout_new +CLUTTER_FLAVOUR +clutter_flow_layout_get_column_spacing +clutter_flow_layout_get_column_width +clutter_flow_layout_get_homogeneous +clutter_flow_layout_get_orientation +clutter_flow_layout_get_row_height +clutter_flow_layout_get_row_spacing +clutter_flow_layout_new +clutter_flow_layout_set_column_spacing +clutter_flow_layout_set_column_width +clutter_flow_layout_set_homogeneous +clutter_flow_layout_set_orientation +clutter_flow_layout_set_row_height +clutter_flow_layout_set_row_spacing +clutter_frame_source_add +clutter_frame_source_add_full +clutter_gdk_disable_event_retrieval +clutter_gdk_get_default_display +clutter_gdk_get_stage_from_window +clutter_gdk_get_stage_window +clutter_gdk_handle_event +clutter_gdk_set_display +clutter_gdk_set_stage_foreign +clutter_geometry_intersects +clutter_geometry_union +clutter_gesture_action_cancel +clutter_gesture_action_get_device +clutter_gesture_action_get_motion_coords +clutter_gesture_action_get_motion_delta +clutter_gesture_action_get_n_current_points +clutter_gesture_action_get_n_touch_points +clutter_gesture_action_get_press_coords +clutter_gesture_action_get_release_coords +clutter_gesture_action_get_sequence +clutter_gesture_action_get_velocity +clutter_gesture_action_new +clutter_gesture_action_set_n_touch_points +clutter_get_accessibility_enabled +clutter_get_actor_by_gid +clutter_get_current_event +clutter_get_current_event_time +clutter_get_debug_enabled +clutter_get_default_backend +clutter_get_default_frame_rate +clutter_get_default_text_direction +clutter_get_font_flags +clutter_get_font_map +clutter_get_input_device_for_id +clutter_get_keyboard_grab +clutter_get_motion_events_enabled +clutter_get_option_group +clutter_get_option_group_without_init +clutter_get_pointer_grab +clutter_get_script_id +clutter_get_show_fps +clutter_get_timestamp +clutter_glx_texture_pixmap_new +clutter_glx_texture_pixmap_new_with_pixmap +clutter_glx_texture_pixmap_new_with_window +clutter_glx_texture_pixmap_using_extension +clutter_grab_keyboard +clutter_grab_pointer +clutter_grab_pointer_for_device +clutter_grid_layout_attach +clutter_grid_layout_attach_next_to +clutter_grid_layout_get_child_at +clutter_grid_layout_get_column_homogeneous +clutter_grid_layout_get_column_spacing +clutter_grid_layout_get_orientation +clutter_grid_layout_get_row_homogeneous +clutter_grid_layout_get_row_spacing +clutter_grid_layout_insert_column +clutter_grid_layout_insert_next_to +clutter_grid_layout_insert_row +clutter_grid_layout_new +clutter_grid_layout_set_column_homogeneous +clutter_grid_layout_set_column_spacing +clutter_grid_layout_set_orientation +clutter_grid_layout_set_row_homogeneous +clutter_grid_layout_set_row_spacing +clutter_group_get_n_children +clutter_group_get_nth_child +clutter_group_new +clutter_group_remove_all +CLUTTER_IMAGE_ERROR +clutter_image_get_texture +clutter_image_new +clutter_image_set_area +clutter_image_set_bytes +clutter_image_set_data +clutter_init +clutter_init_with_args +clutter_input_device_get_associated_device +clutter_input_device_get_axis +clutter_input_device_get_axis_value +clutter_input_device_get_coords +clutter_input_device_get_device_coords +clutter_input_device_get_device_id +clutter_input_device_get_device_mode +clutter_input_device_get_device_name +clutter_input_device_get_device_type +clutter_input_device_get_enabled +clutter_input_device_get_grabbed_actor +clutter_input_device_get_has_cursor +clutter_input_device_get_key +clutter_input_device_get_n_axes +clutter_input_device_get_n_keys +clutter_input_device_get_pointer_actor +clutter_input_device_get_pointer_stage +clutter_input_device_get_slave_devices +clutter_input_device_grab +clutter_input_device_keycode_to_evdev +clutter_input_device_sequence_get_grabbed_actor +clutter_input_device_sequence_grab +clutter_input_device_sequence_ungrab +clutter_input_device_set_enabled +clutter_input_device_set_key +clutter_input_device_ungrab +clutter_input_device_update_from_event +clutter_interval_clone +clutter_interval_compute +clutter_interval_compute_value +clutter_interval_get_final_value +clutter_interval_get_initial_value +clutter_interval_get_interval +clutter_interval_get_value_type +clutter_interval_is_valid +clutter_interval_new +clutter_interval_new_with_values +clutter_interval_peek_final_value +clutter_interval_peek_initial_value +clutter_interval_register_progress_func +clutter_interval_set_final +clutter_interval_set_final_value +clutter_interval_set_initial +clutter_interval_set_initial_value +clutter_interval_set_interval +clutter_interval_validate +clutter_keyframe_transition_clear +clutter_keyframe_transition_get_key_frame +clutter_keyframe_transition_get_n_key_frames +clutter_keyframe_transition_new +clutter_keyframe_transition_set +clutter_keyframe_transition_set_key_frame +clutter_keyframe_transition_set_key_frames +clutter_keyframe_transition_set_modes +clutter_keyframe_transition_set_values +clutter_keysym_to_unicode +clutter_knot_copy +clutter_knot_equal +clutter_knot_free +clutter_layout_manager_allocate +clutter_layout_manager_begin_animation +clutter_layout_manager_child_get +clutter_layout_manager_child_get_property +clutter_layout_manager_child_set +clutter_layout_manager_child_set_property +clutter_layout_manager_end_animation +clutter_layout_manager_find_child_property +clutter_layout_manager_get_animation_progress +clutter_layout_manager_get_child_meta +clutter_layout_manager_get_preferred_height +clutter_layout_manager_get_preferred_width +clutter_layout_manager_layout_changed +clutter_layout_manager_list_child_properties +clutter_layout_manager_set_container +clutter_layout_meta_get_manager +clutter_list_model_new +clutter_list_model_newv +clutter_main +clutter_main_level +clutter_main_quit +CLUTTER_MAJOR_VERSION +clutter_margin_copy +clutter_margin_free +clutter_margin_new +clutter_matrix_alloc +clutter_matrix_free +clutter_matrix_init_from_array +clutter_matrix_init_from_matrix +clutter_matrix_init_identity +clutter_media_get_audio_volume +clutter_media_get_buffer_fill +clutter_media_get_can_seek +clutter_media_get_duration +clutter_media_get_playing +clutter_media_get_progress +clutter_media_get_subtitle_font_name +clutter_media_get_subtitle_uri +clutter_media_get_uri +clutter_media_set_audio_volume +clutter_media_set_filename +clutter_media_set_playing +clutter_media_set_progress +clutter_media_set_subtitle_font_name +clutter_media_set_subtitle_uri +clutter_media_set_uri +CLUTTER_MICRO_VERSION +CLUTTER_MINOR_VERSION +clutter_model_append +clutter_model_appendv +clutter_model_filter_iter +clutter_model_filter_row +clutter_model_foreach +clutter_model_get_column_name +clutter_model_get_column_type +clutter_model_get_filter_set +clutter_model_get_first_iter +clutter_model_get_iter_at_row +clutter_model_get_last_iter +clutter_model_get_n_columns +clutter_model_get_n_rows +clutter_model_get_sorting_column +clutter_model_insert +clutter_model_insertv +clutter_model_insert_value +clutter_model_iter_copy +clutter_model_iter_get +clutter_model_iter_get_model +clutter_model_iter_get_row +clutter_model_iter_get_valist +clutter_model_iter_get_value +clutter_model_iter_is_first +clutter_model_iter_is_last +clutter_model_iter_next +clutter_model_iter_prev +clutter_model_iter_set +clutter_model_iter_set_valist +clutter_model_iter_set_value +clutter_model_prepend +clutter_model_prependv +clutter_model_remove +clutter_model_resort +clutter_model_set_filter +clutter_model_set_names +clutter_model_set_sort +clutter_model_set_sorting_column +clutter_model_set_types +CLUTTER_NO_FPU +clutter_offscreen_effect_create_texture +clutter_offscreen_effect_get_target +clutter_offscreen_effect_get_target_size +clutter_offscreen_effect_get_texture +clutter_offscreen_effect_paint_target +clutter_page_turn_effect_get_angle +clutter_page_turn_effect_get_period +clutter_page_turn_effect_get_radius +clutter_page_turn_effect_new +clutter_page_turn_effect_set_angle +clutter_page_turn_effect_set_period +clutter_page_turn_effect_set_radius +clutter_paint_node_add_child +clutter_paint_node_add_path +clutter_paint_node_add_primitive +clutter_paint_node_add_rectangle +clutter_paint_node_add_texture_rectangle +clutter_paint_node_ref +clutter_paint_node_set_name +clutter_paint_node_unref +clutter_paint_volume_copy +clutter_paint_volume_free +clutter_paint_volume_get_depth +clutter_paint_volume_get_height +clutter_paint_volume_get_origin +clutter_paint_volume_get_width +clutter_paint_volume_set_depth +clutter_paint_volume_set_from_allocation +clutter_paint_volume_set_height +clutter_paint_volume_set_origin +clutter_paint_volume_set_width +clutter_paint_volume_union +clutter_paint_volume_union_box +clutter_pan_action_get_acceleration_factor +clutter_pan_action_get_deceleration +clutter_pan_action_get_interpolate +clutter_pan_action_get_interpolated_coords +clutter_pan_action_get_interpolated_delta +clutter_pan_action_get_pan_axis +clutter_pan_action_new +clutter_pan_action_set_acceleration_factor +clutter_pan_action_set_deceleration +clutter_pan_action_set_interpolate +clutter_pan_action_set_pan_axis +clutter_param_spec_color +clutter_param_spec_fixed +clutter_param_spec_units +clutter_path_add_cairo_path +clutter_path_add_close +clutter_path_add_curve_to +clutter_path_add_line_to +clutter_path_add_move_to +clutter_path_add_node +clutter_path_add_rel_curve_to +clutter_path_add_rel_line_to +clutter_path_add_rel_move_to +clutter_path_add_string +clutter_path_clear +clutter_path_constraint_get_offset +clutter_path_constraint_get_path +clutter_path_constraint_new +clutter_path_constraint_set_offset +clutter_path_constraint_set_path +clutter_path_foreach +clutter_path_get_description +clutter_path_get_length +clutter_path_get_n_nodes +clutter_path_get_node +clutter_path_get_nodes +clutter_path_get_position +clutter_path_insert_node +clutter_path_new +clutter_path_new_with_description +clutter_path_node_copy +clutter_path_node_equal +clutter_path_node_free +clutter_path_remove_node +clutter_path_replace_node +clutter_path_set_description +clutter_path_to_cairo_path +clutter_pipeline_node_new +clutter_point_alloc +clutter_point_copy +clutter_point_distance +clutter_point_equals +clutter_point_free +clutter_point_init +CLUTTER_POINT_INIT +CLUTTER_POINT_INIT_ZERO +clutter_point_zero +CLUTTER_PRIORITY_EVENTS +CLUTTER_PRIORITY_REDRAW +clutter_property_transition_get_property_name +clutter_property_transition_new +clutter_property_transition_set_property_name +clutter_rect_alloc +clutter_rectangle_get_border_color +clutter_rectangle_get_border_width +clutter_rectangle_get_color +clutter_rectangle_new +clutter_rectangle_new_with_color +clutter_rectangle_set_border_color +clutter_rectangle_set_border_width +clutter_rectangle_set_color +clutter_rect_clamp_to_pixel +clutter_rect_contains_point +clutter_rect_contains_rect +clutter_rect_copy +clutter_rect_equals +clutter_rect_free +clutter_rect_get_center +clutter_rect_get_height +clutter_rect_get_width +clutter_rect_get_x +clutter_rect_get_y +clutter_rect_init +CLUTTER_RECT_INIT +CLUTTER_RECT_INIT_ZERO +clutter_rect_inset +clutter_rect_intersection +clutter_rect_normalize +clutter_rect_offset +clutter_rect_union +clutter_rect_zero +clutter_rotate_action_new +clutter_score_append +clutter_score_append_at_marker +clutter_score_get_loop +clutter_score_get_timeline +clutter_score_is_playing +clutter_score_list_timelines +clutter_score_new +clutter_score_pause +clutter_score_remove +clutter_score_remove_all +clutter_score_rewind +clutter_score_set_loop +clutter_score_start +clutter_score_stop +clutter_scriptable_get_id +clutter_scriptable_parse_custom_node +clutter_scriptable_set_custom_property +clutter_scriptable_set_id +clutter_script_add_search_paths +clutter_script_add_states +clutter_script_connect_signals +clutter_script_connect_signals_full +clutter_script_ensure_objects +clutter_script_get_object +clutter_script_get_objects +clutter_script_get_states +clutter_script_get_translation_domain +clutter_script_get_type_from_name +clutter_script_list_objects +clutter_script_load_from_data +clutter_script_load_from_file +clutter_script_load_from_resource +clutter_script_lookup_filename +clutter_script_new +clutter_script_set_translation_domain +clutter_script_unmerge_objects +clutter_scroll_actor_get_scroll_mode +clutter_scroll_actor_new +clutter_scroll_actor_scroll_to_point +clutter_scroll_actor_scroll_to_rect +clutter_scroll_actor_set_scroll_mode +clutter_set_default_frame_rate +clutter_set_font_flags +clutter_set_motion_events_enabled +clutter_settings_get_default +clutter_shader_compile +clutter_shader_effect_get_program +clutter_shader_effect_get_shader +clutter_shader_effect_new +clutter_shader_effect_set_shader_source +clutter_shader_effect_set_uniform +clutter_shader_effect_set_uniform_value +clutter_shader_get_cogl_fragment_shader +clutter_shader_get_cogl_program +clutter_shader_get_cogl_vertex_shader +clutter_shader_get_fragment_source +clutter_shader_get_is_enabled +clutter_shader_get_vertex_source +clutter_shader_is_compiled +clutter_shader_new +clutter_shader_release +clutter_shader_set_fragment_source +clutter_shader_set_is_enabled +clutter_shader_set_uniform +clutter_shader_set_vertex_source +clutter_size_alloc +clutter_size_copy +clutter_size_equals +clutter_size_free +clutter_size_init +CLUTTER_SIZE_INIT +CLUTTER_SIZE_INIT_ZERO +clutter_snap_constraint_get_edges +clutter_snap_constraint_get_offset +clutter_snap_constraint_get_source +clutter_snap_constraint_new +clutter_snap_constraint_set_edges +clutter_snap_constraint_set_offset +clutter_snap_constraint_set_source +clutter_stage_ensure_current +clutter_stage_ensure_redraw +clutter_stage_ensure_viewport +clutter_stage_event +clutter_stage_get_accept_focus +clutter_stage_get_actor_at_pos +clutter_stage_get_color +clutter_stage_get_default +clutter_stage_get_fog +clutter_stage_get_fullscreen +clutter_stage_get_key_focus +clutter_stage_get_minimum_size +clutter_stage_get_motion_events_enabled +clutter_stage_get_no_clear_hint +clutter_stage_get_perspective +clutter_stage_get_redraw_clip_bounds +clutter_stage_get_throttle_motion_events +clutter_stage_get_title +clutter_stage_get_use_alpha +clutter_stage_get_use_fog +clutter_stage_get_user_resizable +CLUTTER_STAGE_HEIGHT +clutter_stage_hide_cursor +clutter_stage_is_default +clutter_stage_manager_get_default +clutter_stage_manager_get_default_stage +clutter_stage_manager_list_stages +clutter_stage_manager_peek_stages +clutter_stage_manager_set_default_stage +clutter_stage_new +clutter_stage_queue_redraw +clutter_stage_read_pixels +clutter_stage_set_accept_focus +clutter_stage_set_color +clutter_stage_set_fog +clutter_stage_set_fullscreen +clutter_stage_set_key_focus +clutter_stage_set_minimum_size +clutter_stage_set_motion_events_enabled +clutter_stage_set_no_clear_hint +clutter_stage_set_perspective +clutter_stage_set_throttle_motion_events +clutter_stage_set_title +clutter_stage_set_use_alpha +clutter_stage_set_use_fog +clutter_stage_set_user_resizable +clutter_stage_show_cursor +CLUTTER_STAGE_WIDTH +clutter_state_get_animator +clutter_state_get_duration +clutter_state_get_keys +clutter_state_get_state +clutter_state_get_states +clutter_state_get_timeline +clutter_state_key_get_mode +clutter_state_key_get_object +clutter_state_key_get_post_delay +clutter_state_key_get_pre_delay +clutter_state_key_get_property_name +clutter_state_key_get_property_type +clutter_state_key_get_source_state_name +clutter_state_key_get_target_state_name +clutter_state_key_get_value +clutter_state_new +clutter_state_remove_key +clutter_state_set +clutter_state_set_animator +clutter_state_set_duration +clutter_state_set_key +clutter_state_set_state +clutter_state_warp_to_state +clutter_swipe_action_new +clutter_table_layout_get_alignment +clutter_table_layout_get_column_count +clutter_table_layout_get_column_spacing +clutter_table_layout_get_easing_duration +clutter_table_layout_get_easing_mode +clutter_table_layout_get_expand +clutter_table_layout_get_fill +clutter_table_layout_get_row_count +clutter_table_layout_get_row_spacing +clutter_table_layout_get_span +clutter_table_layout_get_use_animations +clutter_table_layout_new +clutter_table_layout_pack +clutter_table_layout_set_alignment +clutter_table_layout_set_column_spacing +clutter_table_layout_set_easing_duration +clutter_table_layout_set_easing_mode +clutter_table_layout_set_expand +clutter_table_layout_set_fill +clutter_table_layout_set_row_spacing +clutter_table_layout_set_span +clutter_table_layout_set_use_animations +clutter_text_activate +clutter_text_buffer_delete_text +clutter_text_buffer_emit_deleted_text +clutter_text_buffer_emit_inserted_text +clutter_text_buffer_get_bytes +clutter_text_buffer_get_length +clutter_text_buffer_get_max_length +clutter_text_buffer_get_text +clutter_text_buffer_insert_text +CLUTTER_TEXT_BUFFER_MAX_SIZE +clutter_text_buffer_new +clutter_text_buffer_new_with_text +clutter_text_buffer_set_max_length +clutter_text_buffer_set_text +clutter_text_coords_to_position +clutter_text_delete_chars +clutter_text_delete_selection +clutter_text_delete_text +clutter_text_get_activatable +clutter_text_get_attributes +clutter_text_get_buffer +clutter_text_get_chars +clutter_text_get_color +clutter_text_get_cursor_color +clutter_text_get_cursor_position +clutter_text_get_cursor_size +clutter_text_get_cursor_visible +clutter_text_get_editable +clutter_text_get_ellipsize +clutter_text_get_font_description +clutter_text_get_font_name +clutter_text_get_justify +clutter_text_get_layout +clutter_text_get_layout_offsets +clutter_text_get_line_alignment +clutter_text_get_line_wrap +clutter_text_get_line_wrap_mode +clutter_text_get_max_length +clutter_text_get_password_char +clutter_text_get_selectable +clutter_text_get_selected_text_color +clutter_text_get_selection +clutter_text_get_selection_bound +clutter_text_get_selection_color +clutter_text_get_single_line_mode +clutter_text_get_text +clutter_text_get_use_markup +clutter_text_insert_text +clutter_text_insert_unichar +clutter_text_new +clutter_text_new_full +clutter_text_new_with_buffer +clutter_text_new_with_text +clutter_text_node_new +clutter_text_position_to_coords +clutter_text_set_activatable +clutter_text_set_attributes +clutter_text_set_buffer +clutter_text_set_color +clutter_text_set_cursor_color +clutter_text_set_cursor_position +clutter_text_set_cursor_size +clutter_text_set_cursor_visible +clutter_text_set_editable +clutter_text_set_ellipsize +clutter_text_set_font_description +clutter_text_set_font_name +clutter_text_set_justify +clutter_text_set_line_alignment +clutter_text_set_line_wrap +clutter_text_set_line_wrap_mode +clutter_text_set_markup +clutter_text_set_max_length +clutter_text_set_password_char +clutter_text_set_preedit_string +clutter_text_set_selectable +clutter_text_set_selected_text_color +clutter_text_set_selection +clutter_text_set_selection_bound +clutter_text_set_selection_color +clutter_text_set_single_line_mode +clutter_text_set_text +clutter_text_set_use_markup +CLUTTER_TEXTURE_ERROR +clutter_texture_get_base_size +clutter_texture_get_cogl_material +clutter_texture_get_cogl_texture +clutter_texture_get_filter_quality +clutter_texture_get_keep_aspect_ratio +clutter_texture_get_load_async +clutter_texture_get_load_data_async +clutter_texture_get_max_tile_waste +clutter_texture_get_pick_with_alpha +clutter_texture_get_pixel_format +clutter_texture_get_repeat +clutter_texture_get_sync_size +clutter_texture_new +clutter_texture_new_from_actor +clutter_texture_new_from_file +clutter_texture_node_new +clutter_texture_set_area_from_rgb_data +clutter_texture_set_cogl_material +clutter_texture_set_cogl_texture +clutter_texture_set_filter_quality +clutter_texture_set_from_file +clutter_texture_set_from_rgb_data +clutter_texture_set_from_yuv_data +clutter_texture_set_keep_aspect_ratio +clutter_texture_set_load_async +clutter_texture_set_load_data_async +clutter_texture_set_pick_with_alpha +clutter_texture_set_repeat +clutter_texture_set_sync_size +clutter_threads_add_frame_source +clutter_threads_add_frame_source_full +clutter_threads_add_idle +clutter_threads_add_idle_full +clutter_threads_add_repaint_func +clutter_threads_add_repaint_func_full +clutter_threads_add_timeout +clutter_threads_add_timeout_full +clutter_threads_enter +clutter_threads_init +clutter_threads_leave +clutter_threads_remove_repaint_func +clutter_threads_set_lock_functions +clutter_timeline_add_marker_at_time +clutter_timeline_advance +clutter_timeline_advance_to_marker +clutter_timeline_clone +clutter_timeline_get_auto_reverse +clutter_timeline_get_cubic_bezier_progress +clutter_timeline_get_current_repeat +clutter_timeline_get_delay +clutter_timeline_get_delta +clutter_timeline_get_direction +clutter_timeline_get_duration +clutter_timeline_get_duration_hint +clutter_timeline_get_elapsed_time +clutter_timeline_get_loop +clutter_timeline_get_progress +clutter_timeline_get_progress_mode +clutter_timeline_get_repeat_count +clutter_timeline_get_step_progress +clutter_timeline_has_marker +clutter_timeline_is_playing +clutter_timeline_list_markers +clutter_timeline_new +clutter_timeline_pause +clutter_timeline_remove_marker +clutter_timeline_rewind +clutter_timeline_set_auto_reverse +clutter_timeline_set_cubic_bezier_progress +clutter_timeline_set_delay +clutter_timeline_set_direction +clutter_timeline_set_duration +clutter_timeline_set_loop +clutter_timeline_set_progress_func +clutter_timeline_set_progress_mode +clutter_timeline_set_repeat_count +clutter_timeline_set_step_progress +clutter_timeline_skip +clutter_timeline_start +clutter_timeline_stop +clutter_timeout_pool_add +clutter_timeout_pool_new +clutter_timeout_pool_remove +clutter_transition_get_animatable +clutter_transition_get_interval +clutter_transition_get_remove_on_complete +clutter_transition_group_add_transition +clutter_transition_group_new +clutter_transition_group_remove_all +clutter_transition_group_remove_transition +clutter_transition_set_animatable +clutter_transition_set_from +clutter_transition_set_from_value +clutter_transition_set_interval +clutter_transition_set_remove_on_complete +clutter_transition_set_to +clutter_transition_set_to_value +clutter_ungrab_keyboard +clutter_ungrab_pointer +clutter_ungrab_pointer_for_device +clutter_unicode_to_keysym +clutter_units_copy +clutter_units_free +clutter_units_from_cm +clutter_units_from_em +clutter_units_from_em_for_font +clutter_units_from_mm +clutter_units_from_pixels +clutter_units_from_pt +clutter_units_from_string +clutter_units_get_unit_type +clutter_units_get_unit_value +clutter_units_to_pixels +clutter_units_to_string +clutter_util_next_p2 +clutter_value_dup_paint_node +clutter_value_get_color +clutter_value_get_fixed +clutter_value_get_paint_node +clutter_value_get_shader_float +clutter_value_get_shader_int +clutter_value_get_shader_matrix +clutter_value_get_units +CLUTTER_VALUE_HOLDS_COLOR +CLUTTER_VALUE_HOLDS_FIXED +CLUTTER_VALUE_HOLDS_PAINT_NODE +CLUTTER_VALUE_HOLDS_SHADER_FLOAT +CLUTTER_VALUE_HOLDS_SHADER_INT +CLUTTER_VALUE_HOLDS_SHADER_MATRIX +CLUTTER_VALUE_HOLDS_UNITS +clutter_value_set_color +clutter_value_set_fixed +clutter_value_set_paint_node +clutter_value_set_shader_float +clutter_value_set_shader_int +clutter_value_set_shader_matrix +clutter_value_set_units +clutter_value_take_paint_node +CLUTTER_VERSION +CLUTTER_VERSION_1_0 +CLUTTER_VERSION_1_10 +CLUTTER_VERSION_1_12 +CLUTTER_VERSION_1_2 +CLUTTER_VERSION_1_4 +CLUTTER_VERSION_1_6 +CLUTTER_VERSION_1_8 +CLUTTER_VERSION_HEX +CLUTTER_VERSION_MAX_ALLOWED +CLUTTER_VERSION_MIN_REQUIRED +CLUTTER_VERSION_S +clutter_vertex_alloc +clutter_vertex_copy +clutter_vertex_equal +clutter_vertex_free +clutter_vertex_init +CLUTTER_VERTEX_INIT +CLUTTER_VERTEX_INIT_ZERO +clutter_vertex_new +clutter_wayland_set_compositor_display +clutter_wayland_surface_attach_buffer +clutter_wayland_surface_damage_buffer +clutter_wayland_surface_get_cogl_texture +clutter_wayland_surface_get_surface +clutter_wayland_surface_new +clutter_wayland_surface_set_surface +clutter_win32_disable_event_retrieval +clutter_win32_get_stage_from_window +clutter_win32_get_stage_window +clutter_win32_handle_event +clutter_win32_set_stage_foreign +clutter_x11_add_filter +clutter_x11_disable_event_retrieval +clutter_x11_enable_xinput +clutter_x11_event_get_key_group +clutter_x11_event_sequence_get_touch_detail +clutter_x11_get_current_event_time +clutter_x11_get_default_display +clutter_x11_get_default_screen +clutter_x11_get_input_devices +clutter_x11_get_root_window +clutter_x11_get_stage_from_window +clutter_x11_get_stage_visual +clutter_x11_get_stage_window +clutter_x11_get_use_argb_visual +clutter_x11_get_visual_info +clutter_x11_handle_event +clutter_x11_has_composite_extension +clutter_x11_has_event_retrieval +clutter_x11_has_xinput +clutter_x11_remove_filter +clutter_x11_set_display +clutter_x11_set_stage_foreign +clutter_x11_set_use_argb_visual +clutter_x11_texture_pixmap_new +clutter_x11_texture_pixmap_new_with_pixmap +clutter_x11_texture_pixmap_new_with_window +clutter_x11_texture_pixmap_set_automatic +clutter_x11_texture_pixmap_set_pixmap +clutter_x11_texture_pixmap_set_window +clutter_x11_texture_pixmap_sync_window +clutter_x11_texture_pixmap_update_area +clutter_x11_trap_x_errors +clutter_x11_untrap_x_errors +clutter_zoom_action_get_focal_point +clutter_zoom_action_get_transformed_focal_point +clutter_zoom_action_get_zoom_axis +clutter_zoom_action_new +clutter_zoom_action_set_zoom_axis +CMSG_ALIGN +CMSG_FIRSTHDR +CMSG_NXTHDR +CMSG_SPACE +color_content +color_content_sp +color_map +COLOR_MAP +COLOR_PAIR +COLOR_PAIRS +color_set +COMPILED_SPRITE +config_is_hooked +CPU_ALLOC +CPU_ALLOC_SIZE +CPU_AND +CPU_AND_S +cpu_capabilities +CPU_CLR +CPU_CLR_S +CPU_COUNT +CPU_COUNT_S +CPU_EQUAL +CPU_EQUAL_S +cpu_family +CPU_FREE +CPU_ISSET +CPU_ISSET_S +cpu_model +CPU_OR +CPU_OR_S +CPU_SET +CPU_SET_S +cpu_vendor +CPU_XOR +CPU_XOR_S +CPU_ZERO +CPU_ZERO_S +create_bitmap +create_bitmap_ex +create_blender_table +create_color_table +create_datafile_index +create_light_table +create_lzss_pack_data +create_lzss_unpack_data +create_rgb_table +create_sample +create_scene +create_sub_bitmap +create_sub_zbuffer +create_system_bitmap +create_trans_table +create_video_bitmap +create_zbuffer +cross_product +cross_product_f +crypt_r +ctime_r +curl_easy_cleanup +curl_easy_duphandle +curl_easy_escape +curl_easy_getinfo +curl_easy_init +curl_easy_pause +curl_easy_perform +curl_easy_recv +curl_easy_reset +curl_easy_send +curl_easy_setopt +curl_easy_strerror +curl_easy_unescape +curl_escape +curl_formadd +curl_formfree +curl_formget +curl_free +curl_getdate +curl_getenv +curl_global_cleanup +curl_global_init +curl_global_init_mem +curl_mprintf +curl_multi_add_handle +curl_multi_assign +curl_multi_cleanup +curl_multi_fdset +curl_multi_info_read +curl_multi_init +curl_multi_perform +curl_multi_remove_handle +curl_multi_setopt +curl_multi_socket +curl_multi_socket_action +curl_multi_strerror +curl_multi_timeout +curl_share_cleanup +curl_share_init +curl_share_setopt +curl_share_strerror +curl_slist_append +curl_slist_free_all +curl_strequal +curl_unescape +curl_version +curl_version_info +current_field +current_item +curs_addch +curs_addchstr +curs_addstr +curs_add_wch +curs_add_wchstr +curs_addwstr +curs_attr +curs_beep +curs_bkgd +curs_bkgrnd +curs_border +curs_border_set +curs_clear +curs_color +curs_delch +curs_deleteln +curses_version +curs_extend +curs_getcchar +curs_getch +curs_getstr +curs_get_wch +curs_get_wstr +curs_getyx +curs_inch +curs_inchstr +curs_initscr +curs_inopts +curs_insch +curs_insstr +curs_instr +curs_ins_wch +curs_ins_wstr +curs_in_wch +curs_in_wchstr +curs_inwstr +curs_kernel +curs_legacy +curs_memleaks +curs_mouse +curs_move +curs_opaque +curs_outopts +curs_overlay +curs_pad +curs_print +curs_printw +curs_refresh +curs_scanw +curs_scr_dump +curs_scroll +curs_set +curs_set_sp +curs_slk +curs_sp_funcs +curs_termattrs +curs_termcap +curs_terminfo +curs_threads +curs_touch +curs_trace +curs_util +curs_variables +curs_window +cur_term +data_ahead +data_behind +DAT_ID +DB_File +d_bitmap_proc +DBM_Filter +d_box_proc +d_button_proc +d_check_proc +d_clear_proc +d_ctext_proc +deallocate_voice +d_edit_proc +default_colors +default_palette +define_key +define_key_sp +def_prog_mode +def_prog_mode_sp +def_shell_mode +def_shell_mode_sp +delay_output +delay_output_sp +del_curterm +del_curterm_sp +delete_file +del_panel +des_crypt +DES_FAILED +desktop_color_depth +desktop_palette +des_setparity +destroy_bitmap +destroy_compiled_sprite +destroy_datafile_index +destroy_font +destroy_gfx_mode_list +destroy_midi +destroy_rle_sprite +destroy_sample +destroy_scene +destroy_zbuffer +detect_digi_driver +detect_midi_driver +dialog_message +DIALOG_PLAYER +d_icon_proc +digi_recorder +disable_hardware_cursor +d_keyboard_proc +d_list_proc +dl_iterate_phdr +d_menu_proc +dn_comp +dn_expand +do_arc +do_circle +do_dialog +do_ellipse +do_line +do_menu +dot_product +dot_product_f +do_uconvert +doupdate_sp +d_radio_proc +drand48_r +draw_character_ex +draw_compiled_sprite +draw_gouraud_sprite +drawing_mode +draw_lit_rle_sprite +draw_lit_sprite +draw_rle_sprite +draw_sprite +draw_sprite_ex +draw_sprite_h_flip +draw_sprite_v_flip +draw_sprite_vh_flip +draw_trans_rle_sprite +draw_trans_sprite +d_rtext_proc +d_shadow_box_proc +d_slider_proc +d_textbox_proc +d_text_list_proc +d_text_proc +dup_field +d_yield_proc +dynamic_field_info +ecb_crypt +echo_sp +echo_wchar +ecvt_r +empty_string +enable_hardware_cursor +enable_triple_buffer +encrypt_r +END_OF_FUNCTION +END_OF_MAIN +endwin_sp +envz_add +envz_entry +envz_get +envz_merge +envz_remove +envz_strip +erand48_r +erasechar_sp +error_at_line +error_message_count +error_one_per_line +error_print_progname +ether_aton +ether_aton_r +ether_hostton +ether_line +ether_ntoa +ether_ntoa_r +ether_ntohost +eventfd_read +eventfd_write +extract_font_range +fade_from +fade_from_range +fade_in +fade_in_range +fade_interpolate +fade_out +fade_out_range +__fbufsize +fcvt_r +FD_CLR +FD_ISSET +FD_SET +FD_ZERO +feof_unlocked +ferror_unlocked +ffi_call +ffi_prep_cif +fflush_unlocked +fgetc_unlocked +fgetgrent_r +fgetpwent_r +fgetspent_r +fgets_unlocked +fgetwc_unlocked +fgetws_unlocked +field_arg +field_back +field_buffer +field_count +field_fore +field_index +field_info +field_init +field_just +field_opts +field_opts_off +field_opts_on +field_pad +field_status +field_term +field_type +field_userptr +file_exists +fileno_unlocked +file_select_ex +file_size_ex +file_time +filter_sp +find_allegro_resource +find_datafile_object +find_dialog_focus +fix_filename_case +fix_filename_slashes +fixtorad_r +fixup_datafile +flash_sp +__flbf +fli_bitmap +fli_bmp_dirty_from +fli_bmp_dirty_to +fli_frame +fli_pal_dirty_from +fli_pal_dirty_to +fli_palette +fli_timer +flush_config_file +flushinp_sp +_flushlbf +font_has_alpha +for_each_file_ex +form_cursor +form_data +form_driver +form_field +form_field_attributes +form_field_buffer +form_field_info +form_field_just +form_field_new +form_field_opts +form_fields +form_fieldtype +form_field_userptr +form_field_validation +form_hook +form_init +form_new +form_new_page +form_opts +form_opts_off +form_opts_on +form_page +form_post +form_request_by_name +form_request_name +form_requestname +form_sub +form_term +form_userptr +form_variables +form_win +__fpending +__fpurge +fputc_unlocked +fputs_unlocked +fputwc_unlocked +fputws_unlocked +__freadable +__freading +fread_unlocked +free_audio_stream_buffer +free_config_entries +free_field +free_fieldtype +free_form +free_item +free_lzss_pack_data +free_lzss_unpack_data +free_menu +freeze_mouse_flag +__fsetlocking +fts_children +fts_close +fts_open +fts_read +fts_set +__fwritable +fwrite_unlocked +__fwriting +g_access +g_action_activate +g_action_change_state +GActionEntry +g_action_get_enabled +g_action_get_name +g_action_get_parameter_type +g_action_get_state +g_action_get_state_hint +g_action_get_state_type +g_action_group_action_added +g_action_group_action_enabled_changed +g_action_group_action_removed +g_action_group_action_state_changed +g_action_group_activate_action +g_action_group_change_action_state +g_action_group_get_action_enabled +g_action_group_get_action_parameter_type +g_action_group_get_action_state +g_action_group_get_action_state_hint +g_action_group_get_action_state_type +g_action_group_has_action +GActionGroupInterface +g_action_group_list_actions +g_action_group_query_action +GActionGroup +GActionInterface +g_action_map_add_action +g_action_map_add_action_entries +GActionMapInterface +g_action_map_lookup_action +g_action_map_remove_action +GActionMap +GAction +gai_cancel +gai_error +gai_strerror +gai_suspend +g_alloca +g_app_info_add_supports_type +g_app_info_can_delete +g_app_info_can_remove_supports_type +g_app_info_create_from_commandline +g_app_info_delete +g_app_info_dup +g_app_info_equal +g_app_info_get_all +g_app_info_get_all_for_type +g_app_info_get_commandline +g_app_info_get_default_for_type +g_app_info_get_default_for_uri_scheme +g_app_info_get_description +g_app_info_get_display_name +g_app_info_get_executable +g_app_info_get_fallback_for_type +g_app_info_get_icon +g_app_info_get_id +g_app_info_get_name +g_app_info_get_recommended_for_type +g_app_info_get_supported_types +GAppInfoIface +g_app_info_launch +g_app_info_launch_default_for_uri +g_app_info_launch_uris +g_app_info_remove_supports_type +g_app_info_reset_type_associations +g_app_info_set_as_default_for_extension +g_app_info_set_as_default_for_type +g_app_info_set_as_last_used_for_type +g_app_info_should_show +GAppInfo +g_app_info_supports_files +g_app_info_supports_uris +g_app_launch_context_get_display +g_app_launch_context_get_environment +g_app_launch_context_get_startup_notify_id +g_app_launch_context_launch_failed +g_app_launch_context_new +g_app_launch_context_setenv +GAppLaunchContext +g_app_launch_context_unsetenv +g_application_activate +GApplicationClass +GApplicationCommandLineClass +g_application_command_line_create_file_for_arg +g_application_command_line_get_arguments +g_application_command_line_get_cwd +g_application_command_line_getenv +g_application_command_line_get_environ +g_application_command_line_get_exit_status +g_application_command_line_get_is_remote +g_application_command_line_get_platform_data +g_application_command_line_print +g_application_command_line_printerr +g_application_command_line_set_exit_status +GApplicationCommandLine +g_application_get_application_id +g_application_get_dbus_connection +g_application_get_dbus_object_path +g_application_get_default +g_application_get_flags +g_application_get_inactivity_timeout +g_application_get_is_registered +g_application_get_is_remote +g_application_hold +g_application_id_is_valid +g_application_new +g_application_open +g_application_quit +g_application_register +g_application_release +g_application_run +g_application_set_action_group +g_application_set_application_id +g_application_set_default +g_application_set_flags +g_application_set_inactivity_timeout +GApplication +g_arg_info_get_closure +g_arg_info_get_destroy +g_arg_info_get_direction +g_arg_info_get_ownership_transfer +g_arg_info_get_scope +g_arg_info_get_type +g_arg_info_is_caller_allocates +g_arg_info_is_optional +g_arg_info_is_return_value +g_arg_info_load_type +g_arg_info_may_be_null +GArray +g_array_append_val +g_array_append_vals +g_array_free +g_array_get_element_size +g_array_index +g_array_insert_val +g_array_insert_vals +g_array_new +g_array_prepend_val +g_array_prepend_vals +g_array_ref +g_array_remove_index +g_array_remove_index_fast +g_array_remove_range +g_array_set_clear_func +g_array_set_size +g_array_sized_new +g_array_sort +g_array_sort_with_data +g_array_unref +g_ascii_digit_value +g_ascii_dtostr +G_ASCII_DTOSTR_BUF_SIZE +g_ascii_formatd +g_ascii_isalnum +g_ascii_isalpha +g_ascii_iscntrl +g_ascii_isdigit +g_ascii_isgraph +g_ascii_islower +g_ascii_isprint +g_ascii_ispunct +g_ascii_isspace +g_ascii_isupper +g_ascii_isxdigit +g_ascii_strcasecmp +g_ascii_strdown +g_ascii_strncasecmp +g_ascii_strtod +g_ascii_strtoll +g_ascii_strtoull +g_ascii_strup +g_ascii_tolower +g_ascii_toupper +g_ascii_xdigit_value +g_assert +g_assert_cmpfloat +g_assert_cmphex +g_assert_cmpint +g_assert_cmpstr +g_assert_cmpuint +g_assert_error +g_assert_no_error +g_assert_not_reached +GAsyncInitableIface +g_async_initable_init_async +g_async_initable_init_finish +g_async_initable_new_async +g_async_initable_new_finish +g_async_initable_new_valist_async +g_async_initable_newv_async +GAsyncInitable +GAsyncQueue +g_async_queue_length +g_async_queue_length_unlocked +g_async_queue_lock +g_async_queue_new +g_async_queue_new_full +g_async_queue_pop +g_async_queue_pop_unlocked +g_async_queue_push +g_async_queue_push_sorted +g_async_queue_push_sorted_unlocked +g_async_queue_push_unlocked +g_async_queue_ref +g_async_queue_ref_unlocked +g_async_queue_sort +g_async_queue_sort_unlocked +g_async_queue_timed_pop +g_async_queue_timed_pop_unlocked +g_async_queue_timeout_pop +g_async_queue_timeout_pop_unlocked +g_async_queue_try_pop +g_async_queue_try_pop_unlocked +g_async_queue_unlock +g_async_queue_unref +g_async_queue_unref_and_unlock +GAsyncReadyCallback +g_async_result_get_source_object +g_async_result_get_user_data +GAsyncResultIface +g_async_result_is_tagged +g_async_result_legacy_propagate_error +GAsyncResult +g_atexit +g_atomic_int_add +g_atomic_int_and +g_atomic_int_compare_and_exchange +g_atomic_int_dec_and_test +g_atomic_int_exchange_and_add +g_atomic_int_get +g_atomic_int_inc +g_atomic_int_or +g_atomic_int_set +g_atomic_int_xor +G_ATOMIC_LOCK_FREE +g_atomic_pointer_add +g_atomic_pointer_and +g_atomic_pointer_compare_and_exchange +g_atomic_pointer_get +g_atomic_pointer_or +g_atomic_pointer_set +g_atomic_pointer_xor +g_base64_decode +g_base64_decode_inplace +g_base64_decode_step +g_base64_encode +g_base64_encode_close +g_base64_encode_step +GBaseFinalizeFunc +g_base_info_equal +g_base_info_get_attribute +g_base_info_get_container +g_base_info_get_name +g_base_info_get_namespace +g_base_info_get_type +g_base_info_get_typelib +g_base_info_is_deprecated +g_base_info_iterate_attributes +g_base_info_ref +g_base_info_unref +GBaseInitFunc +g_basename +G_BEGIN_DECLS +G_BIG_ENDIAN +g_binding_get_flags +g_binding_get_source +g_binding_get_source_property +g_binding_get_target +g_binding_get_target_property +GBinding +GBindingTransformFunc +g_bit_lock +g_bit_nth_lsf +g_bit_nth_msf +g_bit_storage +g_bit_trylock +g_bit_unlock +GBookmarkFile +g_bookmark_file_add_application +g_bookmark_file_add_group +G_BOOKMARK_FILE_ERROR +g_bookmark_file_free +g_bookmark_file_get_added +g_bookmark_file_get_app_info +g_bookmark_file_get_applications +g_bookmark_file_get_description +g_bookmark_file_get_groups +g_bookmark_file_get_icon +g_bookmark_file_get_is_private +g_bookmark_file_get_mime_type +g_bookmark_file_get_modified +g_bookmark_file_get_size +g_bookmark_file_get_title +g_bookmark_file_get_uris +g_bookmark_file_get_visited +g_bookmark_file_has_application +g_bookmark_file_has_group +g_bookmark_file_has_item +g_bookmark_file_load_from_data +g_bookmark_file_load_from_data_dirs +g_bookmark_file_load_from_file +g_bookmark_file_move_item +g_bookmark_file_new +g_bookmark_file_remove_application +g_bookmark_file_remove_group +g_bookmark_file_remove_item +g_bookmark_file_set_added +g_bookmark_file_set_app_info +g_bookmark_file_set_description +g_bookmark_file_set_groups +g_bookmark_file_set_icon +g_bookmark_file_set_is_private +g_bookmark_file_set_mime_type +g_bookmark_file_set_modified +g_bookmark_file_set_title +g_bookmark_file_set_visited +g_bookmark_file_to_data +g_bookmark_file_to_file +g_boxed_copy +GBoxedCopyFunc +g_boxed_free +GBoxedFreeFunc +g_boxed_type_register_static +G_BREAKPOINT +g_buffered_input_stream_fill +g_buffered_input_stream_fill_async +g_buffered_input_stream_fill_finish +g_buffered_input_stream_get_available +g_buffered_input_stream_get_buffer_size +g_buffered_input_stream_new +g_buffered_input_stream_new_sized +g_buffered_input_stream_peek +g_buffered_input_stream_peek_buffer +g_buffered_input_stream_read_byte +g_buffered_input_stream_set_buffer_size +GBufferedInputStream +g_buffered_output_stream_get_auto_grow +g_buffered_output_stream_get_buffer_size +g_buffered_output_stream_new +g_buffered_output_stream_new_sized +g_buffered_output_stream_set_auto_grow +g_buffered_output_stream_set_buffer_size +GBufferedOutputStream +g_build_filename +g_build_filenamev +g_build_path +g_build_pathv +GBusAcquiredCallback +g_bus_get +g_bus_get_finish +g_bus_get_sync +GBusNameAcquiredCallback +GBusNameAppearedCallback +GBusNameLostCallback +GBusNameVanishedCallback +g_bus_own_name +g_bus_own_name_on_connection +g_bus_own_name_on_connection_with_closures +g_bus_own_name_with_closures +g_bus_unown_name +g_bus_unwatch_name +g_bus_watch_name +g_bus_watch_name_on_connection +g_bus_watch_name_on_connection_with_closures +g_bus_watch_name_with_closures +GByteArray +g_byte_array_append +g_byte_array_free +g_byte_array_free_to_bytes +g_byte_array_new +g_byte_array_new_take +g_byte_array_prepend +g_byte_array_ref +g_byte_array_remove_index +g_byte_array_remove_index_fast +g_byte_array_remove_range +g_byte_array_set_size +g_byte_array_sized_new +g_byte_array_sort +g_byte_array_sort_with_data +g_byte_array_unref +G_BYTE_ORDER +GBytes +g_bytes_compare +g_bytes_equal +g_bytes_get_data +g_bytes_get_size +g_bytes_hash +g_bytes_new +g_bytes_new_from_bytes +g_bytes_new_static +g_bytes_new_take +g_bytes_new_with_free_func +g_bytes_ref +g_bytes_unref +g_bytes_unref_to_array +g_bytes_unref_to_data +GCache +g_cache_destroy +GCacheDestroyFunc +GCacheDupFunc +g_cache_insert +g_cache_key_foreach +g_cache_new +GCacheNewFunc +g_cache_remove +g_cache_value_foreach +g_callable_info_free_closure +g_callable_info_get_arg +g_callable_info_get_caller_owns +g_callable_info_get_n_args +g_callable_info_get_return_attribute +g_callable_info_get_return_type +g_callable_info_iterate_return_attributes +g_callable_info_load_arg +g_callable_info_load_return_type +g_callable_info_may_return_null +g_callable_info_prepare_closure +GCallback +G_CALLBACK +g_cancellable_cancel +g_cancellable_connect +g_cancellable_disconnect +g_cancellable_get_current +g_cancellable_get_fd +g_cancellable_is_cancelled +g_cancellable_make_pollfd +g_cancellable_new +g_cancellable_pop_current +g_cancellable_push_current +g_cancellable_release_fd +g_cancellable_reset +g_cancellable_set_error_if_cancelled +GCancellableSourceFunc +g_cancellable_source_new +GCancellable +GCClosure +g_cclosure_marshal_BOOL__BOXED_BOXED +g_cclosure_marshal_BOOLEAN__BOXED_BOXED +g_cclosure_marshal_BOOLEAN__BOXED_BOXEDv +g_cclosure_marshal_BOOLEAN__FLAGS +g_cclosure_marshal_BOOLEAN__FLAGSv +g_cclosure_marshal_BOOL__FLAGS +g_cclosure_marshal_generic +g_cclosure_marshal_generic_va +g_cclosure_marshal_STRING__OBJECT_POINTER +g_cclosure_marshal_STRING__OBJECT_POINTERv +g_cclosure_marshal_VOID__BOOLEAN +g_cclosure_marshal_VOID__BOOLEANv +g_cclosure_marshal_VOID__BOXED +g_cclosure_marshal_VOID__BOXEDv +g_cclosure_marshal_VOID__CHAR +g_cclosure_marshal_VOID__CHARv +g_cclosure_marshal_VOID__DOUBLE +g_cclosure_marshal_VOID__DOUBLEv +g_cclosure_marshal_VOID__ENUM +g_cclosure_marshal_VOID__ENUMv +g_cclosure_marshal_VOID__FLAGS +g_cclosure_marshal_VOID__FLAGSv +g_cclosure_marshal_VOID__FLOAT +g_cclosure_marshal_VOID__FLOATv +g_cclosure_marshal_VOID__INT +g_cclosure_marshal_VOID__INTv +g_cclosure_marshal_VOID__LONG +g_cclosure_marshal_VOID__LONGv +g_cclosure_marshal_VOID__OBJECT +g_cclosure_marshal_VOID__OBJECTv +g_cclosure_marshal_VOID__PARAM +g_cclosure_marshal_VOID__PARAMv +g_cclosure_marshal_VOID__POINTER +g_cclosure_marshal_VOID__POINTERv +g_cclosure_marshal_VOID__STRING +g_cclosure_marshal_VOID__STRINGv +g_cclosure_marshal_VOID__UCHAR +g_cclosure_marshal_VOID__UCHARv +g_cclosure_marshal_VOID__UINT +g_cclosure_marshal_VOID__UINT_POINTER +g_cclosure_marshal_VOID__UINT_POINTERv +g_cclosure_marshal_VOID__UINTv +g_cclosure_marshal_VOID__ULONG +g_cclosure_marshal_VOID__ULONGv +g_cclosure_marshal_VOID__VARIANT +g_cclosure_marshal_VOID__VARIANTv +g_cclosure_marshal_VOID__VOID +g_cclosure_marshal_VOID__VOIDv +g_cclosure_new +g_cclosure_new_object +g_cclosure_new_object_swap +g_cclosure_new_swap +G_CCLOSURE_SWAP_DATA +g_charset_converter_get_num_fallbacks +g_charset_converter_get_use_fallback +g_charset_converter_new +g_charset_converter_set_use_fallback +GCharsetConverter +g_chdir +GChecksum +g_checksum_copy +g_checksum_free +g_checksum_get_digest +g_checksum_get_string +g_checksum_new +g_checksum_reset +g_checksum_type_get_length +g_checksum_update +g_child_watch_add +g_child_watch_add_full +GChildWatchFunc +g_child_watch_source_new +g_chmod +GClassFinalizeFunc +GClassInitFunc +g_clear_error +g_clear_object +g_clear_pointer +GClosure +g_closure_add_finalize_notifier +g_closure_add_invalidate_notifier +g_closure_add_marshal_guards +g_closure_invalidate +g_closure_invoke +GClosureMarshal +G_CLOSURE_NEEDS_MARSHAL +g_closure_new_object +g_closure_new_simple +G_CLOSURE_N_NOTIFIERS +GClosureNotify +g_closure_ref +g_closure_remove_finalize_notifier +g_closure_remove_invalidate_notifier +g_closure_set_marshal +g_closure_set_meta_marshal +g_closure_sink +g_closure_unref +GCompareDataFunc +GCompareFunc +GCompletion +g_completion_add_items +g_completion_clear_items +g_completion_complete +g_completion_complete_utf8 +g_completion_free +GCompletionFunc +g_completion_new +g_completion_remove_items +g_completion_set_compare +GCompletionStrncmpFunc +g_compute_checksum_for_bytes +g_compute_checksum_for_data +g_compute_checksum_for_string +g_compute_hmac_for_data +g_compute_hmac_for_string +GCond +g_cond_broadcast +g_cond_clear +g_cond_free +g_cond_init +g_cond_new +g_cond_signal +g_cond_timed_wait +g_cond_wait +g_cond_wait_until +g_constant_info_get_type +g_constant_info_get_value +G_CONST_RETURN +g_content_type_can_be_executable +g_content_type_equals +g_content_type_from_mime_type +g_content_type_get_description +g_content_type_get_generic_icon_name +g_content_type_get_icon +g_content_type_get_mime_type +g_content_type_get_symbolic_icon +g_content_type_guess +g_content_type_guess_for_tree +g_content_type_is_a +g_content_type_is_unknown +g_content_types_get_registered +g_convert +g_converter_convert +GConverterIface +g_converter_input_stream_get_converter +g_converter_input_stream_new +GConverterInputStream +g_converter_output_stream_get_converter +g_converter_output_stream_new +GConverterOutputStream +g_converter_reset +G_CONVERT_ERROR +GConverter +g_convert_with_fallback +g_convert_with_iconv +GCopyFunc +g_creat +g_credentials_get_native +g_credentials_get_unix_pid +g_credentials_get_unix_user +g_credentials_is_same_user +g_credentials_new +g_credentials_set_native +g_credentials_set_unix_user +GCredentials +g_credentials_to_string +g_critical +G_CSET_a_2_z +G_CSET_A_2_Z +G_CSET_DIGITS +G_CSET_LATINC +G_CSET_LATINS +GData +GDataForeachFunc +g_data_input_stream_get_byte_order +g_data_input_stream_get_newline_type +g_data_input_stream_new +g_data_input_stream_read_byte +g_data_input_stream_read_int16 +g_data_input_stream_read_int32 +g_data_input_stream_read_int64 +g_data_input_stream_read_line +g_data_input_stream_read_line_async +g_data_input_stream_read_line_finish +g_data_input_stream_read_line_finish_utf8 +g_data_input_stream_read_line_utf8 +g_data_input_stream_read_uint16 +g_data_input_stream_read_uint32 +g_data_input_stream_read_uint64 +g_data_input_stream_read_until +g_data_input_stream_read_until_async +g_data_input_stream_read_until_finish +g_data_input_stream_read_upto +g_data_input_stream_read_upto_async +g_data_input_stream_read_upto_finish +g_data_input_stream_set_byte_order +g_data_input_stream_set_newline_type +GDataInputStream +g_datalist_clear +G_DATALIST_FLAGS_MASK +g_datalist_foreach +g_datalist_get_data +g_datalist_get_flags +g_datalist_id_dup_data +g_datalist_id_get_data +g_datalist_id_remove_data +g_datalist_id_remove_no_notify +g_datalist_id_replace_data +g_datalist_id_set_data +g_datalist_id_set_data_full +g_datalist_init +g_datalist_remove_data +g_datalist_remove_no_notify +g_datalist_set_data +g_datalist_set_data_full +g_datalist_set_flags +g_datalist_unset_flags +g_data_output_stream_get_byte_order +g_data_output_stream_new +g_data_output_stream_put_byte +g_data_output_stream_put_int16 +g_data_output_stream_put_int32 +g_data_output_stream_put_int64 +g_data_output_stream_put_string +g_data_output_stream_put_uint16 +g_data_output_stream_put_uint32 +g_data_output_stream_put_uint64 +g_data_output_stream_set_byte_order +GDataOutputStream +g_dataset_destroy +g_dataset_foreach +g_dataset_get_data +g_dataset_id_get_data +g_dataset_id_remove_data +g_dataset_id_remove_no_notify +g_dataset_id_set_data +g_dataset_id_set_data_full +g_dataset_remove_data +g_dataset_remove_no_notify +g_dataset_set_data +g_dataset_set_data_full +GDate +g_date_add_days +g_date_add_months +g_date_add_years +G_DATE_BAD_DAY +G_DATE_BAD_JULIAN +G_DATE_BAD_YEAR +g_date_clamp +g_date_clear +g_date_compare +GDateDay +g_date_days_between +g_date_free +g_date_get_day +g_date_get_day_of_year +g_date_get_days_in_month +g_date_get_iso8601_week_of_year +g_date_get_julian +g_date_get_monday_week_of_year +g_date_get_monday_weeks_in_year +g_date_get_month +g_date_get_sunday_week_of_year +g_date_get_sunday_weeks_in_year +g_date_get_weekday +g_date_get_year +g_date_is_first_of_month +g_date_is_last_of_month +g_date_is_leap_year +g_date_new +g_date_new_dmy +g_date_new_julian +g_date_order +g_date_set_day +g_date_set_dmy +g_date_set_julian +g_date_set_month +g_date_set_parse +g_date_set_time +g_date_set_time_t +g_date_set_time_val +g_date_set_year +g_date_strftime +g_date_subtract_days +g_date_subtract_months +g_date_subtract_years +GDateTime +g_date_time_add +g_date_time_add_days +g_date_time_add_full +g_date_time_add_hours +g_date_time_add_minutes +g_date_time_add_months +g_date_time_add_seconds +g_date_time_add_weeks +g_date_time_add_years +g_date_time_compare +g_date_time_difference +g_date_time_equal +g_date_time_format +g_date_time_get_day_of_month +g_date_time_get_day_of_week +g_date_time_get_day_of_year +g_date_time_get_hour +g_date_time_get_microsecond +g_date_time_get_minute +g_date_time_get_month +g_date_time_get_second +g_date_time_get_seconds +g_date_time_get_timezone_abbreviation +g_date_time_get_utc_offset +g_date_time_get_week_numbering_year +g_date_time_get_week_of_year +g_date_time_get_year +g_date_time_get_ymd +g_date_time_hash +g_date_time_is_daylight_savings +g_date_time_new +g_date_time_new_from_timeval_local +g_date_time_new_from_timeval_utc +g_date_time_new_from_unix_local +g_date_time_new_from_unix_utc +g_date_time_new_local +g_date_time_new_now +g_date_time_new_now_local +g_date_time_new_now_utc +g_date_time_new_utc +g_date_time_ref +g_date_time_to_local +g_date_time_to_timeval +g_date_time_to_timezone +g_date_time_to_unix +g_date_time_to_utc +g_date_time_unref +g_date_to_tm +g_date_valid +g_date_valid_day +g_date_valid_dmy +g_date_valid_julian +g_date_valid_month +g_date_valid_weekday +g_date_valid_year +GDateYear +GDBM_File +g_dbus_action_group_get +GDBusActionGroup +g_dbus_address_get_for_bus_sync +g_dbus_address_get_stream +g_dbus_address_get_stream_finish +g_dbus_address_get_stream_sync +g_dbus_annotation_info_lookup +g_dbus_annotation_info_ref +GDBusAnnotationInfo +g_dbus_annotation_info_unref +g_dbus_arg_info_ref +GDBusArgInfo +g_dbus_arg_info_unref +g_dbus_auth_observer_allow_mechanism +g_dbus_auth_observer_authorize_authenticated_peer +g_dbus_auth_observer_new +GDBusAuthObserver +g_dbus_connection_add_filter +g_dbus_connection_call +g_dbus_connection_call_finish +g_dbus_connection_call_sync +g_dbus_connection_call_with_unix_fd_list +g_dbus_connection_call_with_unix_fd_list_finish +g_dbus_connection_call_with_unix_fd_list_sync +g_dbus_connection_close +g_dbus_connection_close_finish +g_dbus_connection_close_sync +g_dbus_connection_emit_signal +g_dbus_connection_export_action_group +g_dbus_connection_export_menu_model +g_dbus_connection_flush +g_dbus_connection_flush_finish +g_dbus_connection_flush_sync +g_dbus_connection_get_capabilities +g_dbus_connection_get_exit_on_close +g_dbus_connection_get_guid +g_dbus_connection_get_last_serial +g_dbus_connection_get_peer_credentials +g_dbus_connection_get_stream +g_dbus_connection_get_unique_name +g_dbus_connection_is_closed +g_dbus_connection_new +g_dbus_connection_new_finish +g_dbus_connection_new_for_address +g_dbus_connection_new_for_address_finish +g_dbus_connection_new_for_address_sync +g_dbus_connection_new_sync +g_dbus_connection_register_object +g_dbus_connection_register_subtree +g_dbus_connection_remove_filter +g_dbus_connection_send_message +g_dbus_connection_send_message_with_reply +g_dbus_connection_send_message_with_reply_finish +g_dbus_connection_send_message_with_reply_sync +g_dbus_connection_set_exit_on_close +g_dbus_connection_signal_subscribe +g_dbus_connection_signal_unsubscribe +g_dbus_connection_start_message_processing +GDBusConnection +g_dbus_connection_unexport_action_group +g_dbus_connection_unexport_menu_model +g_dbus_connection_unregister_object +g_dbus_connection_unregister_subtree +G_DBUS_ERROR +g_dbus_error_encode_gerror +GDBusErrorEntry +g_dbus_error_get_remote_error +g_dbus_error_is_remote_error +g_dbus_error_new_for_dbus_error +g_dbus_error_register_error +g_dbus_error_register_error_domain +g_dbus_error_set_dbus_error +g_dbus_error_set_dbus_error_valist +g_dbus_error_strip_remote_error +g_dbus_error_unregister_error +g_dbus_generate_guid +g_dbus_gvalue_to_gvariant +g_dbus_gvariant_to_gvalue +g_dbus_interface_dup_object +g_dbus_interface_get_info +g_dbus_interface_get_object +GDBusInterfaceGetPropertyFunc +GDBusInterfaceIface +g_dbus_interface_info_cache_build +g_dbus_interface_info_cache_release +g_dbus_interface_info_generate_xml +g_dbus_interface_info_lookup_method +g_dbus_interface_info_lookup_property +g_dbus_interface_info_lookup_signal +g_dbus_interface_info_ref +GDBusInterfaceInfo +g_dbus_interface_info_unref +GDBusInterfaceMethodCallFunc +g_dbus_interface_set_object +GDBusInterfaceSetPropertyFunc +GDBusInterfaceSkeletonClass +g_dbus_interface_skeleton_export +g_dbus_interface_skeleton_flush +g_dbus_interface_skeleton_get_connection +g_dbus_interface_skeleton_get_connections +g_dbus_interface_skeleton_get_flags +g_dbus_interface_skeleton_get_info +g_dbus_interface_skeleton_get_object_path +g_dbus_interface_skeleton_get_properties +g_dbus_interface_skeleton_get_vtable +g_dbus_interface_skeleton_has_connection +g_dbus_interface_skeleton_set_flags +GDBusInterfaceSkeleton +g_dbus_interface_skeleton_unexport +g_dbus_interface_skeleton_unexport_from_connection +GDBusInterface +GDBusInterfaceVTable +g_dbus_is_address +g_dbus_is_guid +g_dbus_is_interface_name +g_dbus_is_member_name +g_dbus_is_name +g_dbus_is_supported_address +g_dbus_is_unique_name +g_dbus_menu_model_get +GDBusMenuModel +g_dbus_message_bytes_needed +g_dbus_message_copy +GDBusMessageFilterFunction +g_dbus_message_get_arg0 +g_dbus_message_get_body +g_dbus_message_get_byte_order +g_dbus_message_get_destination +g_dbus_message_get_error_name +g_dbus_message_get_flags +g_dbus_message_get_header +g_dbus_message_get_header_fields +g_dbus_message_get_interface +g_dbus_message_get_locked +g_dbus_message_get_member +g_dbus_message_get_message_type +g_dbus_message_get_num_unix_fds +g_dbus_message_get_path +g_dbus_message_get_reply_serial +g_dbus_message_get_sender +g_dbus_message_get_serial +g_dbus_message_get_signature +g_dbus_message_get_unix_fd_list +g_dbus_message_lock +g_dbus_message_new +g_dbus_message_new_from_blob +g_dbus_message_new_method_call +g_dbus_message_new_method_error +g_dbus_message_new_method_error_literal +g_dbus_message_new_method_error_valist +g_dbus_message_new_method_reply +g_dbus_message_new_signal +g_dbus_message_print +g_dbus_message_set_body +g_dbus_message_set_byte_order +g_dbus_message_set_destination +g_dbus_message_set_error_name +g_dbus_message_set_flags +g_dbus_message_set_header +g_dbus_message_set_interface +g_dbus_message_set_member +g_dbus_message_set_message_type +g_dbus_message_set_num_unix_fds +g_dbus_message_set_path +g_dbus_message_set_reply_serial +g_dbus_message_set_sender +g_dbus_message_set_serial +g_dbus_message_set_signature +g_dbus_message_set_unix_fd_list +GDBusMessage +g_dbus_message_to_blob +g_dbus_message_to_gerror +g_dbus_method_info_ref +GDBusMethodInfo +g_dbus_method_info_unref +g_dbus_method_invocation_get_connection +g_dbus_method_invocation_get_interface_name +g_dbus_method_invocation_get_message +g_dbus_method_invocation_get_method_info +g_dbus_method_invocation_get_method_name +g_dbus_method_invocation_get_object_path +g_dbus_method_invocation_get_parameters +g_dbus_method_invocation_get_sender +g_dbus_method_invocation_get_user_data +g_dbus_method_invocation_return_dbus_error +g_dbus_method_invocation_return_error +g_dbus_method_invocation_return_error_literal +g_dbus_method_invocation_return_error_valist +g_dbus_method_invocation_return_gerror +g_dbus_method_invocation_return_value +g_dbus_method_invocation_return_value_with_unix_fd_list +GDBusMethodInvocation +g_dbus_method_invocation_take_error +g_dbus_node_info_generate_xml +g_dbus_node_info_lookup_interface +g_dbus_node_info_new_for_xml +g_dbus_node_info_ref +GDBusNodeInfo +g_dbus_node_info_unref +g_dbus_object_get_interface +g_dbus_object_get_interfaces +g_dbus_object_get_object_path +GDBusObjectIface +GDBusObjectManagerClientClass +g_dbus_object_manager_client_get_connection +g_dbus_object_manager_client_get_flags +g_dbus_object_manager_client_get_name +g_dbus_object_manager_client_get_name_owner +g_dbus_object_manager_client_new +g_dbus_object_manager_client_new_finish +g_dbus_object_manager_client_new_for_bus +g_dbus_object_manager_client_new_for_bus_finish +g_dbus_object_manager_client_new_for_bus_sync +g_dbus_object_manager_client_new_sync +GDBusObjectManagerClient +g_dbus_object_manager_get_interface +g_dbus_object_manager_get_object +g_dbus_object_manager_get_object_path +g_dbus_object_manager_get_objects +GDBusObjectManagerIface +GDBusObjectManagerServerClass +g_dbus_object_manager_server_export +g_dbus_object_manager_server_export_uniquely +g_dbus_object_manager_server_get_connection +g_dbus_object_manager_server_is_exported +g_dbus_object_manager_server_new +g_dbus_object_manager_server_set_connection +GDBusObjectManagerServer +g_dbus_object_manager_server_unexport +GDBusObjectManager +GDBusObjectProxyClass +g_dbus_object_proxy_get_connection +g_dbus_object_proxy_new +GDBusObjectProxy +g_dbus_object_skeleton_add_interface +GDBusObjectSkeletonClass +g_dbus_object_skeleton_flush +g_dbus_object_skeleton_new +g_dbus_object_skeleton_remove_interface +g_dbus_object_skeleton_remove_interface_by_name +g_dbus_object_skeleton_set_object_path +GDBusObjectSkeleton +GDBusObject +g_dbus_property_info_ref +GDBusPropertyInfo +g_dbus_property_info_unref +g_dbus_proxy_call +g_dbus_proxy_call_finish +g_dbus_proxy_call_sync +g_dbus_proxy_call_with_unix_fd_list +g_dbus_proxy_call_with_unix_fd_list_finish +g_dbus_proxy_call_with_unix_fd_list_sync +GDBusProxyClass +g_dbus_proxy_get_cached_property +g_dbus_proxy_get_cached_property_names +g_dbus_proxy_get_connection +g_dbus_proxy_get_default_timeout +g_dbus_proxy_get_flags +g_dbus_proxy_get_interface_info +g_dbus_proxy_get_interface_name +g_dbus_proxy_get_name +g_dbus_proxy_get_name_owner +g_dbus_proxy_get_object_path +g_dbus_proxy_new +g_dbus_proxy_new_finish +g_dbus_proxy_new_for_bus +g_dbus_proxy_new_for_bus_finish +g_dbus_proxy_new_for_bus_sync +g_dbus_proxy_new_sync +g_dbus_proxy_set_cached_property +g_dbus_proxy_set_default_timeout +g_dbus_proxy_set_interface_info +GDBusProxy +GDBusProxyTypeFunc +g_dbus_server_get_client_address +g_dbus_server_get_flags +g_dbus_server_get_guid +g_dbus_server_is_active +g_dbus_server_new_sync +g_dbus_server_start +g_dbus_server_stop +GDBusServer +GDBusSignalCallback +g_dbus_signal_info_ref +GDBusSignalInfo +g_dbus_signal_info_unref +GDBusSubtreeDispatchFunc +GDBusSubtreeEnumerateFunc +GDBusSubtreeIntrospectFunc +GDBusSubtreeVTable +g_dcgettext +g_debug +GDebugKey +G_DEFINE_ABSTRACT_TYPE +G_DEFINE_ABSTRACT_TYPE_WITH_CODE +G_DEFINE_BOXED_TYPE +G_DEFINE_BOXED_TYPE_WITH_CODE +G_DEFINE_DYNAMIC_TYPE +G_DEFINE_DYNAMIC_TYPE_EXTENDED +G_DEFINE_INTERFACE +G_DEFINE_INTERFACE_WITH_CODE +G_DEFINE_POINTER_TYPE +G_DEFINE_POINTER_TYPE_WITH_CODE +G_DEFINE_QUARK +G_DEFINE_TYPE +G_DEFINE_TYPE_EXTENDED +G_DEFINE_TYPE_WITH_CODE +G_DEPRECATED +G_DEPRECATED_FOR +g_desktop_app_info_get_boolean +g_desktop_app_info_get_categories +g_desktop_app_info_get_filename +g_desktop_app_info_get_generic_name +g_desktop_app_info_get_is_hidden +g_desktop_app_info_get_keywords +g_desktop_app_info_get_nodisplay +g_desktop_app_info_get_show_in +g_desktop_app_info_get_startup_wm_class +g_desktop_app_info_get_string +g_desktop_app_info_has_key +g_desktop_app_info_launch_uris_as_manager +g_desktop_app_info_new +g_desktop_app_info_new_from_filename +g_desktop_app_info_new_from_keyfile +g_desktop_app_info_set_desktop_env +GDesktopAppInfo +GDesktopAppLaunchCallback +GDestroyNotify +g_dgettext +GDir +g_dir_close +g_direct_equal +g_direct_hash +g_dir_make_tmp +g_dirname +g_dir_open +g_dir_read_name +g_dir_rewind +G_DIR_SEPARATOR +G_DIR_SEPARATOR_S +GdkAppLaunchContext +gdk_app_launch_context_new +gdk_app_launch_context_set_desktop +gdk_app_launch_context_set_display +gdk_app_launch_context_set_icon +gdk_app_launch_context_set_icon_name +gdk_app_launch_context_set_screen +gdk_app_launch_context_set_timestamp +GdkAtom +gdk_atom_intern +gdk_atom_intern_static_string +gdk_atom_name +GDK_ATOM_TO_POINTER +gdk_beep +GDK_BUTTON_MIDDLE +GDK_BUTTON_PRIMARY +GDK_BUTTON_SECONDARY +gdk_cairo_create +gdk_cairo_get_clip_rectangle +gdk_cairo_rectangle +gdk_cairo_region +gdk_cairo_region_create_from_surface +gdk_cairo_set_source_color +gdk_cairo_set_source_pixbuf +gdk_cairo_set_source_rgba +gdk_cairo_set_source_window +GdkColor +gdk_color_copy +gdk_color_equal +gdk_color_free +gdk_color_hash +gdk_color_parse +gdk_color_to_string +GDK_CURRENT_TIME +GdkCursor +gdk_cursor_get_cursor_type +gdk_cursor_get_display +gdk_cursor_get_image +gdk_cursor_new +gdk_cursor_new_for_display +gdk_cursor_new_from_name +gdk_cursor_new_from_pixbuf +gdk_cursor_ref +gdk_cursor_unref +GDK_CURSOR_XCURSOR +GDK_CURSOR_XDISPLAY +GdkDevice +gdk_device_free_history +gdk_device_get_associated_device +gdk_device_get_axis +gdk_device_get_axis_use +gdk_device_get_axis_value +gdk_device_get_device_type +gdk_device_get_display +gdk_device_get_has_cursor +gdk_device_get_history +gdk_device_get_key +gdk_device_get_mode +gdk_device_get_name +gdk_device_get_n_axes +gdk_device_get_n_keys +gdk_device_get_position +gdk_device_get_source +gdk_device_get_state +gdk_device_get_window_at_position +gdk_device_grab +gdk_device_list_axes +gdk_device_list_slave_devices +GdkDeviceManager +gdk_device_manager_get_client_pointer +gdk_device_manager_get_display +gdk_device_manager_list_devices +gdk_device_set_axis_use +gdk_device_set_key +gdk_device_set_mode +gdk_device_ungrab +gdk_device_warp +gdk_disable_multidevice +GdkDisplay +gdk_display_beep +gdk_display_close +gdk_display_device_is_grabbed +gdk_display_flush +gdk_display_get_app_launch_context +gdk_display_get_default +gdk_display_get_default_cursor_size +gdk_display_get_default_group +gdk_display_get_default_screen +gdk_display_get_device_manager +gdk_display_get_event +gdk_display_get_maximal_cursor_size +gdk_display_get_name +gdk_display_get_n_screens +gdk_display_get_pointer +gdk_display_get_screen +gdk_display_get_window_at_pointer +gdk_display_has_pending +gdk_display_is_closed +gdk_display_keyboard_ungrab +gdk_display_list_devices +GdkDisplayManager +gdk_display_manager_get +gdk_display_manager_get_default_display +gdk_display_manager_list_displays +gdk_display_manager_open_display +gdk_display_manager_set_default_display +gdk_display_notify_startup_complete +gdk_display_open +gdk_display_peek_event +gdk_display_pointer_is_grabbed +gdk_display_pointer_ungrab +gdk_display_put_event +gdk_display_request_selection_notification +gdk_display_set_double_click_distance +gdk_display_set_double_click_time +gdk_display_store_clipboard +gdk_display_supports_clipboard_persistence +gdk_display_supports_composite +gdk_display_supports_cursor_alpha +gdk_display_supports_cursor_color +gdk_display_supports_input_shapes +gdk_display_supports_selection_notification +gdk_display_supports_shapes +gdk_display_sync +gdk_display_warp_pointer +GDK_DISPLAY_XDISPLAY +gdk_drag_abort +gdk_drag_begin +gdk_drag_begin_for_device +GdkDragContext +gdk_drag_context_get_actions +gdk_drag_context_get_dest_window +gdk_drag_context_get_device +gdk_drag_context_get_protocol +gdk_drag_context_get_selected_action +gdk_drag_context_get_source_window +gdk_drag_context_get_suggested_action +gdk_drag_context_list_targets +gdk_drag_context_set_device +gdk_drag_drop +gdk_drag_drop_succeeded +gdk_drag_find_window_for_screen +gdk_drag_get_selection +gdk_drag_motion +gdk_drag_status +gdk_drop_finish +gdk_drop_reply +gdk_error_trap_pop +gdk_error_trap_pop_ignored +gdk_error_trap_push +GdkEventAny +GdkEventButton +GdkEventConfigure +gdk_event_copy +GdkEventCrossing +GdkEventDND +GdkEventExpose +GdkEventFocus +gdk_event_free +GdkEventFunc +gdk_event_get +gdk_event_get_axis +gdk_event_get_button +gdk_event_get_click_count +gdk_event_get_coords +gdk_event_get_device +gdk_event_get_event_sequence +gdk_event_get_keycode +gdk_event_get_keyval +gdk_event_get_root_coords +gdk_event_get_screen +gdk_event_get_scroll_deltas +gdk_event_get_scroll_direction +gdk_event_get_source_device +gdk_event_get_state +gdk_event_get_time +GdkEventGrabBroken +gdk_event_handler_set +GdkEventKey +GdkEventMotion +gdk_event_new +GdkEventOwnerChange +gdk_event_peek +GDK_EVENT_PROPAGATE +GdkEventProperty +GdkEventProximity +gdk_event_put +gdk_event_request_motions +GdkEventScroll +GdkEventSelection +GdkEventSequence +gdk_event_set_device +gdk_event_set_screen +gdk_event_set_source_device +GdkEventSetting +gdk_events_get_angle +gdk_events_get_center +gdk_events_get_distance +gdk_events_pending +GDK_EVENT_STOP +GdkEventTouch +gdk_event_triggers_context_menu +GdkEventVisibility +GdkEventWindowState +GdkFilterFunc +gdk_flush +GdkGeometry +gdk_get_default_root_window +gdk_get_display +gdk_get_display_arg_name +gdk_get_program_class +gdk_get_show_events +gdk_init +gdk_init_check +gdk_keyboard_grab +gdk_keyboard_ungrab +GdkKeymap +gdk_keymap_add_virtual_modifiers +gdk_keymap_get_caps_lock_state +gdk_keymap_get_default +gdk_keymap_get_direction +gdk_keymap_get_entries_for_keycode +gdk_keymap_get_entries_for_keyval +gdk_keymap_get_for_display +gdk_keymap_get_modifier_mask +gdk_keymap_get_modifier_state +gdk_keymap_get_num_lock_state +gdk_keymap_have_bidi_layouts +GdkKeymapKey +gdk_keymap_lookup_key +gdk_keymap_map_virtual_modifiers +gdk_keymap_translate_keyboard_state +gdk_keyval_convert_case +gdk_keyval_from_name +gdk_keyval_is_lower +gdk_keyval_is_upper +gdk_keyval_name +gdk_keyval_to_lower +gdk_keyval_to_unicode +gdk_keyval_to_upper +gdk_list_visuals +GDK_NONE +gdk_notify_startup_complete +gdk_notify_startup_complete_with_id +gdk_offscreen_window_get_embedder +gdk_offscreen_window_get_surface +gdk_offscreen_window_set_embedder +gdk_pango_context_get +gdk_pango_context_get_for_screen +gdk_pango_layout_get_clip_region +gdk_pango_layout_line_get_clip_region +GDK_PARENT_RELATIVE +gdk_parse_args +gdk_pixbuf_get_from_surface +gdk_pixbuf_get_from_window +GdkPoint +gdk_pointer_grab +gdk_pointer_is_grabbed +GDK_POINTER_TO_ATOM +GDK_POINTER_TO_XID +gdk_pointer_ungrab +GDK_PRIORITY_EVENTS +GDK_PRIORITY_REDRAW +gdk_property_change +gdk_property_delete +gdk_property_get +gdk_query_depths +gdk_query_visual_types +GdkRectangle +gdk_rectangle_intersect +gdk_rectangle_union +GdkRGBA +gdk_rgba_copy +gdk_rgba_equal +gdk_rgba_free +gdk_rgba_hash +gdk_rgba_parse +gdk_rgba_to_string +GDK_ROOT_WINDOW +GdkScreen +gdk_screen_get_active_window +gdk_screen_get_default +gdk_screen_get_display +gdk_screen_get_font_options +gdk_screen_get_height +gdk_screen_get_height_mm +gdk_screen_get_monitor_at_point +gdk_screen_get_monitor_at_window +gdk_screen_get_monitor_geometry +gdk_screen_get_monitor_height_mm +gdk_screen_get_monitor_plug_name +gdk_screen_get_monitor_width_mm +gdk_screen_get_monitor_workarea +gdk_screen_get_n_monitors +gdk_screen_get_number +gdk_screen_get_primary_monitor +gdk_screen_get_resolution +gdk_screen_get_rgba_visual +gdk_screen_get_root_window +gdk_screen_get_setting +gdk_screen_get_system_visual +gdk_screen_get_toplevel_windows +gdk_screen_get_width +gdk_screen_get_width_mm +gdk_screen_get_window_stack +gdk_screen_height +gdk_screen_height_mm +gdk_screen_is_composited +gdk_screen_list_visuals +gdk_screen_make_display_name +gdk_screen_set_font_options +gdk_screen_set_resolution +gdk_screen_width +gdk_screen_width_mm +GDK_SCREEN_XDISPLAY +GDK_SCREEN_XNUMBER +GDK_SCREEN_XSCREEN +GDK_SELECTION_CLIPBOARD +gdk_selection_convert +gdk_selection_owner_get +gdk_selection_owner_get_for_display +gdk_selection_owner_set +gdk_selection_owner_set_for_display +GDK_SELECTION_PRIMARY +gdk_selection_property_get +GDK_SELECTION_SECONDARY +gdk_selection_send_notify +gdk_selection_send_notify_for_display +GDK_SELECTION_TYPE_ATOM +GDK_SELECTION_TYPE_BITMAP +GDK_SELECTION_TYPE_COLORMAP +GDK_SELECTION_TYPE_DRAWABLE +GDK_SELECTION_TYPE_INTEGER +GDK_SELECTION_TYPE_PIXMAP +GDK_SELECTION_TYPE_STRING +GDK_SELECTION_TYPE_WINDOW +gdk_set_double_click_time +gdk_set_program_class +gdk_set_show_events +gdk_setting_get +GDK_TARGET_BITMAP +GDK_TARGET_COLORMAP +GDK_TARGET_DRAWABLE +GDK_TARGET_PIXMAP +GDK_TARGET_STRING +gdk_test_render_sync +gdk_test_simulate_button +gdk_test_simulate_key +gdk_text_property_to_utf8_list_for_display +gdk_threads_add_idle +gdk_threads_add_idle_full +gdk_threads_add_timeout +gdk_threads_add_timeout_full +gdk_threads_add_timeout_seconds +gdk_threads_add_timeout_seconds_full +gdk_threads_enter +GDK_THREADS_ENTER +gdk_threads_init +gdk_threads_leave +GDK_THREADS_LEAVE +gdk_threads_set_lock_functions +GdkTimeCoord +gdk_unicode_to_keyval +gdk_utf8_to_string_target +GDK_VERSION_3_0 +GDK_VERSION_3_2 +GDK_VERSION_3_4 +GDK_VERSION_MAX_ALLOWED +GDK_VERSION_MIN_REQUIRED +GdkVisual +gdk_visual_get_best +gdk_visual_get_best_depth +gdk_visual_get_best_type +gdk_visual_get_best_with_both +gdk_visual_get_best_with_depth +gdk_visual_get_best_with_type +gdk_visual_get_bits_per_rgb +gdk_visual_get_blue_pixel_details +gdk_visual_get_byte_order +gdk_visual_get_colormap_size +gdk_visual_get_depth +gdk_visual_get_green_pixel_details +gdk_visual_get_red_pixel_details +gdk_visual_get_screen +gdk_visual_get_system +gdk_visual_get_visual_type +GdkWindow +gdk_window_add_filter +gdk_window_at_pointer +GdkWindowAttr +gdk_window_beep +gdk_window_begin_move_drag +gdk_window_begin_move_drag_for_device +gdk_window_begin_paint_rect +gdk_window_begin_paint_region +gdk_window_begin_resize_drag +gdk_window_begin_resize_drag_for_device +GdkWindowChildFunc +gdk_window_configure_finished +gdk_window_constrain_size +gdk_window_coords_from_parent +gdk_window_coords_to_parent +gdk_window_create_similar_surface +gdk_window_deiconify +gdk_window_destroy +gdk_window_enable_synchronized_configure +gdk_window_end_paint +gdk_window_ensure_native +gdk_window_flush +gdk_window_focus +gdk_window_freeze_updates +gdk_window_fullscreen +gdk_window_geometry_changed +gdk_window_get_accept_focus +gdk_window_get_background_pattern +gdk_window_get_children +gdk_window_get_clip_region +gdk_window_get_composited +gdk_window_get_cursor +gdk_window_get_decorations +gdk_window_get_device_cursor +gdk_window_get_device_events +gdk_window_get_device_position +gdk_window_get_display +gdk_window_get_drag_protocol +gdk_window_get_effective_parent +gdk_window_get_effective_toplevel +gdk_window_get_events +gdk_window_get_focus_on_map +gdk_window_get_frame_extents +gdk_window_get_geometry +gdk_window_get_group +gdk_window_get_height +gdk_window_get_modal_hint +gdk_window_get_origin +gdk_window_get_parent +gdk_window_get_pointer +gdk_window_get_position +gdk_window_get_root_coords +gdk_window_get_root_origin +gdk_window_get_screen +gdk_window_get_source_events +gdk_window_get_state +gdk_window_get_support_multidevice +gdk_window_get_toplevel +gdk_window_get_type_hint +gdk_window_get_update_area +gdk_window_get_user_data +gdk_window_get_visible_region +gdk_window_get_visual +gdk_window_get_width +gdk_window_get_window_type +gdk_window_has_native +gdk_window_hide +gdk_window_iconify +GDK_WINDOWING_WIN32 +GDK_WINDOWING_X11 +gdk_window_input_shape_combine_region +gdk_window_invalidate_maybe_recurse +gdk_window_invalidate_rect +gdk_window_invalidate_region +gdk_window_is_destroyed +gdk_window_is_input_only +gdk_window_is_shaped +gdk_window_is_viewable +gdk_window_is_visible +gdk_window_lower +gdk_window_maximize +gdk_window_merge_child_input_shapes +gdk_window_merge_child_shapes +gdk_window_move +gdk_window_move_region +gdk_window_move_resize +gdk_window_new +gdk_window_peek_children +gdk_window_process_all_updates +gdk_window_process_updates +gdk_window_raise +gdk_window_register_dnd +gdk_window_remove_filter +gdk_window_reparent +gdk_window_resize +gdk_window_restack +gdk_window_scroll +gdk_window_set_accept_focus +gdk_window_set_background +gdk_window_set_background_pattern +gdk_window_set_background_rgba +gdk_window_set_child_input_shapes +gdk_window_set_child_shapes +gdk_window_set_composited +gdk_window_set_cursor +gdk_window_set_debug_updates +gdk_window_set_decorations +gdk_window_set_device_cursor +gdk_window_set_device_events +gdk_window_set_events +gdk_window_set_focus_on_map +gdk_window_set_functions +gdk_window_set_geometry_hints +gdk_window_set_group +gdk_window_set_icon_list +gdk_window_set_icon_name +gdk_window_set_keep_above +gdk_window_set_keep_below +gdk_window_set_modal_hint +gdk_window_set_opacity +gdk_window_set_override_redirect +gdk_window_set_role +gdk_window_set_skip_pager_hint +gdk_window_set_skip_taskbar_hint +gdk_window_set_source_events +gdk_window_set_startup_id +gdk_window_set_static_gravities +gdk_window_set_support_multidevice +gdk_window_set_title +gdk_window_set_transient_for +gdk_window_set_type_hint +gdk_window_set_urgency_hint +gdk_window_set_user_data +gdk_window_shape_combine_region +gdk_window_show +gdk_window_show_unraised +gdk_window_stick +gdk_window_thaw_updates +gdk_window_unfullscreen +gdk_window_unmaximize +gdk_window_unstick +gdk_window_withdraw +GDK_WINDOW_XID +gdk_x11_atom_to_xatom +gdk_x11_atom_to_xatom_for_display +gdk_x11_cursor_get_xcursor +gdk_x11_cursor_get_xdisplay +gdk_x11_device_get_id +gdk_x11_device_manager_lookup +gdk_x11_display_broadcast_startup_message +gdk_x11_display_error_trap_pop +gdk_x11_display_error_trap_pop_ignored +gdk_x11_display_error_trap_push +gdk_x11_display_get_startup_notification_id +gdk_x11_display_get_user_time +gdk_x11_display_get_xdisplay +gdk_x11_display_grab +gdk_x11_display_set_cursor_theme +gdk_x11_display_set_startup_notification_id +gdk_x11_display_string_to_compound_text +gdk_x11_display_text_property_to_text_list +gdk_x11_display_ungrab +gdk_x11_display_utf8_to_compound_text +gdk_x11_free_compound_text +gdk_x11_free_text_list +gdk_x11_get_default_root_xwindow +gdk_x11_get_default_screen +gdk_x11_get_default_xdisplay +gdk_x11_get_server_time +gdk_x11_get_xatom_by_name +gdk_x11_get_xatom_by_name_for_display +gdk_x11_get_xatom_name +gdk_x11_get_xatom_name_for_display +gdk_x11_grab_server +gdk_x11_keymap_get_group_for_state +gdk_x11_keymap_key_is_modifier +gdk_x11_lookup_xdisplay +gdk_x11_register_standard_event_type +gdk_x11_screen_get_monitor_output +gdk_x11_screen_get_screen_number +gdk_x11_screen_get_window_manager_name +gdk_x11_screen_get_xscreen +gdk_x11_screen_lookup_visual +gdk_x11_screen_supports_net_wm_hint +gdk_x11_set_sm_client_id +gdk_x11_ungrab_server +gdk_x11_visual_get_xvisual +gdk_x11_window_foreign_new_for_display +gdk_x11_window_get_xid +gdk_x11_window_lookup_for_display +gdk_x11_window_move_to_current_desktop +gdk_x11_window_set_hide_titlebar_when_maximized +gdk_x11_window_set_theme_variant +gdk_x11_window_set_user_time +gdk_x11_xatom_to_atom +gdk_x11_xatom_to_atom_for_display +GdkXEvent +GDK_XID_TO_POINTER +g_dngettext +g_double_equal +g_double_hash +g_dpgettext +g_dpgettext2 +g_drive_can_eject +g_drive_can_poll_for_media +g_drive_can_start +g_drive_can_start_degraded +g_drive_can_stop +g_drive_eject +g_drive_eject_finish +g_drive_eject_with_operation +g_drive_eject_with_operation_finish +g_drive_enumerate_identifiers +g_drive_get_icon +g_drive_get_identifier +g_drive_get_name +g_drive_get_sort_key +g_drive_get_start_stop_type +g_drive_get_symbolic_icon +g_drive_get_volumes +g_drive_has_media +g_drive_has_volumes +GDriveIface +g_drive_is_media_check_automatic +g_drive_is_media_removable +g_drive_poll_for_media +g_drive_poll_for_media_finish +g_drive_start +g_drive_start_finish +g_drive_stop +g_drive_stop_finish +GDrive +GDuplicateFunc +G_E +g_emblemed_icon_add_emblem +g_emblemed_icon_clear_emblems +g_emblemed_icon_get_emblems +g_emblemed_icon_get_icon +g_emblemed_icon_new +GEmblemedIcon +g_emblem_get_icon +g_emblem_get_origin +g_emblem_new +g_emblem_new_with_origin +GEmblem +G_END_DECLS +generate_332_palette +generate_optimized_palette +GEnumClass +G_ENUM_CLASS +G_ENUM_CLASS_TYPE +G_ENUM_CLASS_TYPE_NAME +g_enum_complete_type_info +g_enum_get_value +g_enum_get_value_by_name +g_enum_get_value_by_nick +g_enum_info_get_method +g_enum_info_get_n_methods +g_enum_info_get_n_values +g_enum_info_get_storage_type +g_enum_info_get_value +g_enum_register_static +GEnumValue +g_environ_getenv +g_environ_setenv +g_environ_unsetenv +GEqualFunc +g_error +GError +g_error_copy +g_error_free +g_error_matches +g_error_new +g_error_new_literal +g_error_new_valist +getaddrinfo_a +geta_depth +getaliasbyname_r +getaliasent_r +get_align_matrix +get_align_matrix_f +get_audio_stream_buffer +getb_depth +get_camera_matrix +get_camera_matrix_f +getchar_unlocked +get_clip_rect +get_clip_state +get_color +get_color_conversion +get_color_depth +get_compiled_sprite +get_config_argv +get_config_float +get_config_hex +get_config_id +get_config_int +get_config_string +get_config_text +getc_unlocked +get_current_dir_name +get_datafile_property +getdate_err +getdate_r +get_desktop_resolution +get_display_switch_mode +get_escdelay +get_escdelay_sp +get_executable_name +get_extension +get_filename +get_filename_encoding +get_font_range_begin +get_font_range_end +get_font_ranges +getg_depth +get_gfx_mode +get_gfx_mode_list +get_gfx_mode_type +getgrent_r +getgrgid_r +getgrnam_r +get_hardware_volume +gethostbyaddr_r +gethostbyname2_r +gethostbyname_r +gethostent_r +getlogin_r +get_midi_length +get_mixer_bits +get_mixer_buffer_length +get_mixer_channels +get_mixer_frequency +get_mixer_quality +get_mixer_voices +getmntent_r +get_mouse_mickeys +getmouse_sp +get_myaddress +getnetbyaddr_r +getnetbyname_r +getnetent_r +getnetgrent_r +get_nprocs +get_nprocs_conf +getn_wstr +getopt_long +getopt_long_only +get_palette +get_palette_range +_getpixel +_getpixel15 +_getpixel16 +_getpixel24 +_getpixel32 +getprotobyname_r +getprotobynumber_r +getprotoent_r +getpwent_r +getpwnam_r +getpwuid_r +getr_depth +get_refresh_rate +get_rle_sprite +get_rotation_matrix +get_rotation_matrix_f +get_rotation_quat +getrpcbyname_r +getrpcbynumber_r +getrpcent_r +get_scaling_matrix +get_scaling_matrix_f +getservbyname_r +getservbyport_r +getservent_r +get_sound_input_cap_bits +get_sound_input_cap_parm +get_sound_input_cap_rate +get_sound_input_cap_stereo +getspent_r +getspnam_r +gettext_macro +get_transformation_matrix +get_transformation_matrix_f +get_translation_matrix +get_translation_matrix_f +get_uformat +getutent_r +getutid_r +getutline_r +get_vector_rotation_matrix +get_vector_rotation_matrix_f +get_vector_rotation_quat +get_volume +get_wch +getwchar_unlocked +getwc_unlocked +getwin_sp +get_wstr +get_x_rotate_matrix +get_x_rotate_matrix_f +get_x_rotate_quat +get_y_rotate_matrix +get_y_rotate_matrix_f +get_y_rotate_quat +get_z_rotate_matrix +get_z_rotate_matrix_f +get_z_rotate_quat +g_field_info_get_field +g_field_info_get_flags +g_field_info_get_offset +g_field_info_get_size +g_field_info_get_type +g_field_info_set_field +g_file_append_to +g_file_append_to_async +g_file_append_to_finish +G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE +G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE +G_FILE_ATTRIBUTE_ACCESS_CAN_READ +G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME +G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH +G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE +G_FILE_ATTRIBUTE_DOS_IS_ARCHIVE +G_FILE_ATTRIBUTE_DOS_IS_SYSTEM +G_FILE_ATTRIBUTE_ETAG_VALUE +G_FILE_ATTRIBUTE_FILESYSTEM_FREE +G_FILE_ATTRIBUTE_FILESYSTEM_READONLY +G_FILE_ATTRIBUTE_FILESYSTEM_SIZE +G_FILE_ATTRIBUTE_FILESYSTEM_TYPE +G_FILE_ATTRIBUTE_FILESYSTEM_USED +G_FILE_ATTRIBUTE_FILESYSTEM_USE_PREVIEW +G_FILE_ATTRIBUTE_GVFS_BACKEND +G_FILE_ATTRIBUTE_ID_FILE +G_FILE_ATTRIBUTE_ID_FILESYSTEM +GFileAttributeInfo +GFileAttributeInfoList +g_file_attribute_info_list_add +g_file_attribute_info_list_dup +g_file_attribute_info_list_lookup +g_file_attribute_info_list_new +g_file_attribute_info_list_ref +g_file_attribute_info_list_unref +g_file_attribute_matcher_enumerate_namespace +g_file_attribute_matcher_enumerate_next +g_file_attribute_matcher_matches +g_file_attribute_matcher_matches_only +g_file_attribute_matcher_new +g_file_attribute_matcher_ref +GFileAttributeMatcher +g_file_attribute_matcher_subtract +g_file_attribute_matcher_to_string +g_file_attribute_matcher_unref +G_FILE_ATTRIBUTE_MOUNTABLE_CAN_EJECT +G_FILE_ATTRIBUTE_MOUNTABLE_CAN_MOUNT +G_FILE_ATTRIBUTE_MOUNTABLE_CAN_POLL +G_FILE_ATTRIBUTE_MOUNTABLE_CAN_START +G_FILE_ATTRIBUTE_MOUNTABLE_CAN_START_DEGRADED +G_FILE_ATTRIBUTE_MOUNTABLE_CAN_STOP +G_FILE_ATTRIBUTE_MOUNTABLE_CAN_UNMOUNT +G_FILE_ATTRIBUTE_MOUNTABLE_HAL_UDI +G_FILE_ATTRIBUTE_MOUNTABLE_IS_MEDIA_CHECK_AUTOMATIC +G_FILE_ATTRIBUTE_MOUNTABLE_START_STOP_TYPE +G_FILE_ATTRIBUTE_MOUNTABLE_UNIX_DEVICE +G_FILE_ATTRIBUTE_MOUNTABLE_UNIX_DEVICE_FILE +G_FILE_ATTRIBUTE_OWNER_GROUP +G_FILE_ATTRIBUTE_OWNER_USER +G_FILE_ATTRIBUTE_OWNER_USER_REAL +G_FILE_ATTRIBUTE_PREVIEW_ICON +G_FILE_ATTRIBUTE_SELINUX_CONTEXT +G_FILE_ATTRIBUTE_STANDARD_ALLOCATED_SIZE +G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE +G_FILE_ATTRIBUTE_STANDARD_COPY_NAME +G_FILE_ATTRIBUTE_STANDARD_DESCRIPTION +G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME +G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME +G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE +G_FILE_ATTRIBUTE_STANDARD_ICON +G_FILE_ATTRIBUTE_STANDARD_IS_BACKUP +G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN +G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK +G_FILE_ATTRIBUTE_STANDARD_IS_VIRTUAL +G_FILE_ATTRIBUTE_STANDARD_NAME +G_FILE_ATTRIBUTE_STANDARD_SIZE +G_FILE_ATTRIBUTE_STANDARD_SORT_ORDER +G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON +G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET +G_FILE_ATTRIBUTE_STANDARD_TARGET_URI +G_FILE_ATTRIBUTE_STANDARD_TYPE +G_FILE_ATTRIBUTE_THUMBNAILING_FAILED +G_FILE_ATTRIBUTE_THUMBNAIL_PATH +G_FILE_ATTRIBUTE_TIME_ACCESS +G_FILE_ATTRIBUTE_TIME_ACCESS_USEC +G_FILE_ATTRIBUTE_TIME_CHANGED +G_FILE_ATTRIBUTE_TIME_CHANGED_USEC +G_FILE_ATTRIBUTE_TIME_CREATED +G_FILE_ATTRIBUTE_TIME_CREATED_USEC +G_FILE_ATTRIBUTE_TIME_MODIFIED +G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC +G_FILE_ATTRIBUTE_TRASH_DELETION_DATE +G_FILE_ATTRIBUTE_TRASH_ITEM_COUNT +G_FILE_ATTRIBUTE_TRASH_ORIG_PATH +G_FILE_ATTRIBUTE_UNIX_BLOCKS +G_FILE_ATTRIBUTE_UNIX_BLOCK_SIZE +G_FILE_ATTRIBUTE_UNIX_DEVICE +G_FILE_ATTRIBUTE_UNIX_GID +G_FILE_ATTRIBUTE_UNIX_INODE +G_FILE_ATTRIBUTE_UNIX_IS_MOUNTPOINT +G_FILE_ATTRIBUTE_UNIX_MODE +G_FILE_ATTRIBUTE_UNIX_NLINK +G_FILE_ATTRIBUTE_UNIX_RDEV +G_FILE_ATTRIBUTE_UNIX_UID +g_file_copy +g_file_copy_async +g_file_copy_attributes +g_file_copy_finish +g_file_create +g_file_create_async +g_file_create_finish +g_file_create_readwrite +g_file_create_readwrite_async +g_file_create_readwrite_finish +g_file_delete +g_file_delete_async +g_file_delete_finish +g_file_descriptor_based_get_fd +GFileDescriptorBased +g_file_dup +g_file_eject_mountable +g_file_eject_mountable_finish +g_file_eject_mountable_with_operation +g_file_eject_mountable_with_operation_finish +g_file_enumerate_children +g_file_enumerate_children_async +g_file_enumerate_children_finish +g_file_enumerator_close +g_file_enumerator_close_async +g_file_enumerator_close_finish +g_file_enumerator_get_child +g_file_enumerator_get_container +g_file_enumerator_has_pending +g_file_enumerator_is_closed +g_file_enumerator_next_file +g_file_enumerator_next_files_async +g_file_enumerator_next_files_finish +g_file_enumerator_set_pending +GFileEnumerator +g_file_equal +G_FILE_ERROR +g_file_error_from_errno +g_file_find_enclosing_mount +g_file_find_enclosing_mount_async +g_file_find_enclosing_mount_finish +g_file_get_basename +g_file_get_child +g_file_get_child_for_display_name +g_file_get_contents +g_file_get_parent +g_file_get_parse_name +g_file_get_path +g_file_get_relative_path +g_file_get_uri +g_file_get_uri_scheme +g_file_hash +g_file_has_parent +g_file_has_prefix +g_file_has_uri_scheme +g_file_icon_get_file +g_file_icon_new +GFileIcon +GFileIface +g_file_info_clear_status +g_file_info_copy_into +g_file_info_dup +g_file_info_get_attribute_as_string +g_file_info_get_attribute_boolean +g_file_info_get_attribute_byte_string +g_file_info_get_attribute_data +g_file_info_get_attribute_int32 +g_file_info_get_attribute_int64 +g_file_info_get_attribute_object +g_file_info_get_attribute_status +g_file_info_get_attribute_string +g_file_info_get_attribute_stringv +g_file_info_get_attribute_type +g_file_info_get_attribute_uint32 +g_file_info_get_attribute_uint64 +g_file_info_get_content_type +g_file_info_get_deletion_date +g_file_info_get_display_name +g_file_info_get_edit_name +g_file_info_get_etag +g_file_info_get_file_type +g_file_info_get_icon +g_file_info_get_is_backup +g_file_info_get_is_hidden +g_file_info_get_is_symlink +g_file_info_get_modification_time +g_file_info_get_name +g_file_info_get_size +g_file_info_get_sort_order +g_file_info_get_symbolic_icon +g_file_info_get_symlink_target +g_file_info_has_attribute +g_file_info_has_namespace +g_file_info_list_attributes +g_file_info_new +g_file_info_remove_attribute +g_file_info_set_attribute +g_file_info_set_attribute_boolean +g_file_info_set_attribute_byte_string +g_file_info_set_attribute_int32 +g_file_info_set_attribute_int64 +g_file_info_set_attribute_mask +g_file_info_set_attribute_object +g_file_info_set_attribute_status +g_file_info_set_attribute_string +g_file_info_set_attribute_stringv +g_file_info_set_attribute_uint32 +g_file_info_set_attribute_uint64 +g_file_info_set_content_type +g_file_info_set_display_name +g_file_info_set_edit_name +g_file_info_set_file_type +g_file_info_set_icon +g_file_info_set_is_hidden +g_file_info_set_is_symlink +g_file_info_set_modification_time +g_file_info_set_name +g_file_info_set_size +g_file_info_set_sort_order +g_file_info_set_symbolic_icon +g_file_info_set_symlink_target +GFileInfo +g_file_info_unset_attribute_mask +g_file_input_stream_query_info +g_file_input_stream_query_info_async +g_file_input_stream_query_info_finish +GFileInputStream +g_file_io_stream_get_etag +g_file_io_stream_query_info +g_file_io_stream_query_info_async +g_file_io_stream_query_info_finish +GFileIOStream +g_file_is_native +g_file_load_contents +g_file_load_contents_async +g_file_load_contents_finish +g_file_load_partial_contents_async +g_file_load_partial_contents_finish +g_file_make_directory +g_file_make_directory_with_parents +g_file_make_symbolic_link +g_file_monitor +g_file_monitor_cancel +g_file_monitor_directory +g_file_monitor_emit_event +g_file_monitor_file +g_file_monitor_is_cancelled +g_file_monitor_set_rate_limit +GFileMonitor +g_file_mount_enclosing_volume +g_file_mount_enclosing_volume_finish +g_file_mount_mountable +g_file_mount_mountable_finish +g_file_move +g_filename_completer_get_completions +g_filename_completer_get_completion_suffix +g_filename_completer_new +g_filename_completer_set_dirs_only +GFilenameCompleter +g_filename_display_basename +g_filename_display_name +g_filename_from_uri +g_filename_from_utf8 +g_filename_to_uri +g_filename_to_utf8 +g_file_new_for_commandline_arg +g_file_new_for_commandline_arg_and_cwd +g_file_new_for_path +g_file_new_for_uri +g_file_new_tmp +g_file_open_readwrite +g_file_open_readwrite_async +g_file_open_readwrite_finish +g_file_open_tmp +g_file_output_stream_get_etag +g_file_output_stream_query_info +g_file_output_stream_query_info_async +g_file_output_stream_query_info_finish +GFileOutputStream +g_file_parse_name +g_file_poll_mountable +g_file_poll_mountable_finish +GFileProgressCallback +g_file_query_default_handler +g_file_query_exists +g_file_query_filesystem_info +g_file_query_filesystem_info_async +g_file_query_filesystem_info_finish +g_file_query_file_type +g_file_query_info +g_file_query_info_async +g_file_query_info_finish +g_file_query_settable_attributes +g_file_query_writable_namespaces +g_file_read +g_file_read_async +g_file_read_finish +g_file_read_link +GFileReadMoreCallback +g_file_replace +g_file_replace_async +g_file_replace_contents +g_file_replace_contents_async +g_file_replace_contents_finish +g_file_replace_finish +g_file_replace_readwrite +g_file_replace_readwrite_async +g_file_replace_readwrite_finish +g_file_resolve_relative_path +g_file_set_attribute +g_file_set_attribute_byte_string +g_file_set_attribute_int32 +g_file_set_attribute_int64 +g_file_set_attributes_async +g_file_set_attributes_finish +g_file_set_attributes_from_info +g_file_set_attribute_string +g_file_set_attribute_uint32 +g_file_set_attribute_uint64 +g_file_set_contents +g_file_set_display_name +g_file_set_display_name_async +g_file_set_display_name_finish +g_file_start_mountable +g_file_start_mountable_finish +g_file_stop_mountable +g_file_stop_mountable_finish +GFile +g_file_supports_thread_contexts +g_file_test +g_file_trash +g_file_unmount_mountable +g_file_unmount_mountable_finish +g_file_unmount_mountable_with_operation +g_file_unmount_mountable_with_operation_finish +g_filter_input_stream_get_base_stream +g_filter_input_stream_get_close_base_stream +g_filter_input_stream_set_close_base_stream +GFilterInputStream +g_filter_output_stream_get_base_stream +g_filter_output_stream_get_close_base_stream +g_filter_output_stream_set_close_base_stream +GFilterOutputStream +g_find_program_in_path +GFlagsClass +G_FLAGS_CLASS +G_FLAGS_CLASS_TYPE +G_FLAGS_CLASS_TYPE_NAME +g_flags_complete_type_info +g_flags_get_first_value +g_flags_get_value_by_name +g_flags_get_value_by_nick +g_flags_register_static +GFlagsValue +g_fopen +g_format_size +g_format_size_for_display +g_format_size_full +g_fprintf +g_free +GFreeFunc +g_freopen +GFunc +g_function_info_get_flags +g_function_info_get_property +g_function_info_get_symbol +g_function_info_get_vfunc +g_function_info_invoke +g_function_info_prep_invoker +g_function_invoker_destroy +gfx_capabilities +GFX_MODE +GFX_MODE +GFX_MODE_LIST +GFX_MODE_LIST +gfx_mode_select +gfx_mode_select_ex +gfx_mode_select_filter +g_get_application_name +g_get_charset +g_get_codeset +g_get_current_dir +g_get_current_time +g_getenv +g_get_environ +g_get_filename_charsets +g_get_home_dir +g_get_host_name +g_get_language_names +g_get_locale_variants +g_get_monotonic_time +g_get_num_processors +g_get_prgname +g_get_real_name +g_get_real_time +g_get_system_config_dirs +g_get_system_data_dirs +g_get_tmp_dir +g_get_user_cache_dir +g_get_user_config_dir +g_get_user_data_dir +g_get_user_name +g_get_user_runtime_dir +g_get_user_special_dir +G_GINT16_FORMAT +G_GINT16_MODIFIER +G_GINT32_FORMAT +G_GINT32_MODIFIER +G_GINT64_CONSTANT +G_GINT64_FORMAT +G_GINT64_MODIFIER +G_GINTPTR_FORMAT +G_GINTPTR_MODIFIER +G_GNUC_ALLOC_SIZE +G_GNUC_ALLOC_SIZE2 +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +G_GNUC_CONST +G_GNUC_DEPRECATED +G_GNUC_DEPRECATED_FOR +G_GNUC_END_IGNORE_DEPRECATIONS +G_GNUC_EXTENSION +G_GNUC_FORMAT +G_GNUC_FUNCTION +G_GNUC_INTERNAL +G_GNUC_MALLOC +G_GNUC_MAY_ALIAS +G_GNUC_NO_INSTRUMENT +G_GNUC_NORETURN +G_GNUC_NULL_TERMINATED +G_GNUC_PRETTY_FUNCTION +G_GNUC_PRINTF +G_GNUC_PURE +G_GNUC_SCANF +G_GNUC_UNUSED +G_GNUC_WARN_UNUSED_RESULT +G_GOFFSET_CONSTANT +G_GOFFSET_FORMAT +G_GOFFSET_MODIFIER +G_GSIZE_FORMAT +G_GSIZE_MODIFIER +G_GSSIZE_FORMAT +G_GUINT16_FORMAT +G_GUINT32_FORMAT +G_GUINT64_CONSTANT +G_GUINT64_FORMAT +G_GUINTPTR_FORMAT +GHashFunc +GHashTable +g_hash_table_add +g_hash_table_contains +g_hash_table_destroy +g_hash_table_find +g_hash_table_foreach +g_hash_table_foreach_remove +g_hash_table_foreach_steal +g_hash_table_freeze +g_hash_table_get_keys +g_hash_table_get_values +g_hash_table_insert +GHashTableIter +g_hash_table_iter_get_hash_table +g_hash_table_iter_init +g_hash_table_iter_next +g_hash_table_iter_remove +g_hash_table_iter_replace +g_hash_table_iter_steal +g_hash_table_lookup +g_hash_table_lookup_extended +g_hash_table_new +g_hash_table_new_full +g_hash_table_ref +g_hash_table_remove +g_hash_table_remove_all +g_hash_table_replace +g_hash_table_size +g_hash_table_steal +g_hash_table_steal_all +g_hash_table_thaw +g_hash_table_unref +G_HAVE_GNUC_VISIBILITY +GHFunc +GHmac +g_hmac_copy +g_hmac_get_digest +g_hmac_get_string +g_hmac_new +g_hmac_ref +g_hmac_unref +g_hmac_update +GHook +G_HOOK +G_HOOK_ACTIVE +g_hook_alloc +g_hook_append +GHookCheckFunc +GHookCheckMarshaller +GHookCompareFunc +g_hook_compare_ids +g_hook_destroy +g_hook_destroy_link +GHookFinalizeFunc +g_hook_find +g_hook_find_data +g_hook_find_func +GHookFindFunc +g_hook_find_func_data +g_hook_first_valid +G_HOOK_FLAGS +G_HOOK_FLAG_USER_SHIFT +g_hook_free +GHookFunc +g_hook_get +G_HOOK_IN_CALL +g_hook_insert_before +g_hook_insert_sorted +G_HOOK_IS_UNLINKED +G_HOOK_IS_VALID +GHookList +g_hook_list_clear +g_hook_list_init +g_hook_list_invoke +g_hook_list_invoke_check +g_hook_list_marshal +g_hook_list_marshal_check +GHookMarshaller +g_hook_next_valid +g_hook_prepend +g_hook_ref +g_hook_unref +g_hostname_is_ascii_encoded +g_hostname_is_ip_address +g_hostname_is_non_ascii +g_hostname_to_ascii +g_hostname_to_unicode +GHRFunc +g_htonl +g_htons +gi_cclosure_marshal_generic +g_icon_equal +g_icon_hash +GIconIface +g_icon_new_for_string +GIcon +g_icon_to_string +g_iconv +GIConv +g_iconv_close +g_iconv_open +g_idle_add +g_idle_add_full +g_idle_remove_by_data +g_idle_source_new +G_IEEE754_DOUBLE_BIAS +G_IEEE754_FLOAT_BIAS +GI_IS_ARG_INFO +GI_IS_CALLABLE_INFO +GI_IS_CONSTANT_INFO +GI_IS_ENUM_INFO +GI_IS_FIELD_INFO +GI_IS_FUNCTION_INFO +GI_IS_INTERFACE_INFO +GI_IS_OBJECT_INFO +GI_IS_PROPERTY_INFO +GI_IS_REGISTERED_TYPE_INFO +GI_IS_SIGNAL_INFO +GI_IS_STRUCT_INFO +GI_IS_TYPE_INFO +GI_IS_UNION_INFO +GI_IS_VALUE_INFO +GI_IS_VFUNC_INFO +G_IMPLEMENT_INTERFACE +G_IMPLEMENT_INTERFACE_DYNAMIC +g_inet_address_equal +g_inet_address_get_family +g_inet_address_get_is_any +g_inet_address_get_is_link_local +g_inet_address_get_is_loopback +g_inet_address_get_is_mc_global +g_inet_address_get_is_mc_link_local +g_inet_address_get_is_mc_node_local +g_inet_address_get_is_mc_org_local +g_inet_address_get_is_mc_site_local +g_inet_address_get_is_multicast +g_inet_address_get_is_site_local +g_inet_address_get_native_size +GInetAddressMask +g_inet_address_mask_equal +g_inet_address_mask_get_address +g_inet_address_mask_get_family +g_inet_address_mask_get_length +g_inet_address_mask_matches +g_inet_address_mask_new +g_inet_address_mask_new_from_string +g_inet_address_mask_to_string +g_inet_address_new_any +g_inet_address_new_from_bytes +g_inet_address_new_from_string +g_inet_address_new_loopback +GInetAddress +g_inet_address_to_bytes +g_inet_address_to_string +g_inet_socket_address_get_address +g_inet_socket_address_get_flowinfo +g_inet_socket_address_get_port +g_inet_socket_address_get_scope_id +g_inet_socket_address_new +GInetSocketAddress +g_info_find_method +g_info_get_alignment +g_info_get_field +g_info_get_method +g_info_get_n_fields +g_info_get_n_methods +g_info_get_size +g_info_is_foreign +g_info_is_gtype +g_info_type_to_string +GInitableIface +g_initable_init +g_initable_new +g_initable_newv +g_initable_new_valist +GInitable +GInitiallyUnowned +GInitiallyUnownedClass +G_INLINE_FUNC +g_input_stream_clear_pending +g_input_stream_close +g_input_stream_close_async +g_input_stream_close_finish +g_input_stream_has_pending +g_input_stream_is_closed +g_input_stream_read +g_input_stream_read_all +g_input_stream_read_async +g_input_stream_read_bytes +g_input_stream_read_bytes_async +g_input_stream_read_bytes_finish +g_input_stream_read_finish +g_input_stream_set_pending +g_input_stream_skip +g_input_stream_skip_async +g_input_stream_skip_finish +GInputStream +GInputVector +GInstanceInitFunc +GINT16_FROM_BE +GINT16_FROM_LE +GINT16_TO_BE +GINT16_TO_LE +GINT32_FROM_BE +GINT32_FROM_LE +GINT32_TO_BE +GINT32_TO_LE +g_int64_equal +GINT64_FROM_BE +GINT64_FROM_LE +g_int64_hash +GINT64_TO_BE +GINT64_TO_LE +g_int_equal +GInterfaceFinalizeFunc +GInterfaceInfo +g_interface_info_find_method +g_interface_info_find_signal +g_interface_info_find_vfunc +g_interface_info_get_constant +g_interface_info_get_iface +g_interface_info_get_method +g_interface_info_get_n_constants +g_interface_info_get_n_methods +g_interface_info_get_n_prerequisites +g_interface_info_get_n_properties +g_interface_info_get_n_signals +g_interface_info_get_n_vfuncs +g_interface_info_get_prerequisite +g_interface_info_get_property +g_interface_info_get_signal +g_interface_info_get_vfunc +GInterfaceInitFunc +g_intern_static_string +g_intern_string +GINT_FROM_BE +GINT_FROM_LE +g_int_hash +GINT_TO_BE +GINT_TO_LE +GINT_TO_POINTER +G_INVOKE_ERROR +g_io_add_watch +g_io_add_watch_full +GIOChannel +g_io_channel_close +G_IO_CHANNEL_ERROR +g_io_channel_error_from_errno +g_io_channel_flush +g_io_channel_get_buffer_condition +g_io_channel_get_buffered +g_io_channel_get_buffer_size +g_io_channel_get_close_on_unref +g_io_channel_get_encoding +g_io_channel_get_flags +g_io_channel_get_line_term +g_io_channel_init +g_io_channel_new_file +g_io_channel_read +g_io_channel_read_chars +g_io_channel_read_line +g_io_channel_read_line_string +g_io_channel_read_to_end +g_io_channel_read_unichar +g_io_channel_ref +g_io_channel_seek +g_io_channel_seek_position +g_io_channel_set_buffered +g_io_channel_set_buffer_size +g_io_channel_set_close_on_unref +g_io_channel_set_encoding +g_io_channel_set_flags +g_io_channel_set_line_term +g_io_channel_shutdown +g_io_channel_unix_get_fd +g_io_channel_unix_new +g_io_channel_unref +g_io_channel_win32_new_fd +g_io_channel_win32_new_messages +g_io_channel_win32_new_socket +g_io_channel_write +g_io_channel_write_chars +g_io_channel_write_unichar +g_io_create_watch +G_IO_ERROR +g_io_error_from_errno +g_io_error_from_win32_error +GIOExtension +g_io_extension_get_name +g_io_extension_get_priority +g_io_extension_get_type +GIOExtensionPoint +g_io_extension_point_get_extension_by_name +g_io_extension_point_get_extensions +g_io_extension_point_get_required_type +g_io_extension_point_implement +g_io_extension_point_lookup +g_io_extension_point_register +g_io_extension_point_set_required_type +g_io_extension_ref_class +GIOFunc +GIOFuncs +g_io_module_load +g_io_module_new +g_io_module_query +GIOModuleScope +g_io_module_scope_block +g_io_module_scope_free +g_io_module_scope_new +g_io_modules_load_all_in_directory +g_io_modules_load_all_in_directory_with_scope +g_io_modules_scan_all_in_directory +g_io_modules_scan_all_in_directory_with_scope +GIOModule +g_io_module_unload +g_io_scheduler_cancel_all_jobs +GIOSchedulerJob +GIOSchedulerJobFunc +g_io_scheduler_job_send_to_mainloop +g_io_scheduler_job_send_to_mainloop_async +g_io_scheduler_push_job +g_io_stream_clear_pending +g_io_stream_close +g_io_stream_close_async +g_io_stream_close_finish +g_io_stream_get_input_stream +g_io_stream_get_output_stream +g_io_stream_has_pending +g_io_stream_is_closed +g_io_stream_set_pending +g_io_stream_splice_async +g_io_stream_splice_finish +GIOStream +g_irepository_dump +g_irepository_enumerate_versions +G_IREPOSITORY_ERROR +g_irepository_find_by_gtype +g_irepository_find_by_name +g_irepository_get_c_prefix +g_irepository_get_default +g_irepository_get_dependencies +g_irepository_get_info +g_irepository_get_loaded_namespaces +g_irepository_get_n_infos +g_irepository_get_option_group +g_irepository_get_search_path +g_irepository_get_shared_library +g_irepository_get_typelib_path +g_irepository_get_version +g_irepository_is_registered +g_irepository_load_typelib +g_irepository_prepend_search_path +g_irepository_require +g_irepository_require_private +G_IR_MAGIC +G_IS_DIR_SEPARATOR +G_IS_ENUM_CLASS +G_IS_FLAGS_CLASS +G_IS_OBJECT +G_IS_OBJECT_CLASS +G_IS_PARAM_SPEC +G_IS_PARAM_SPEC_BOOLEAN +G_IS_PARAM_SPEC_BOXED +G_IS_PARAM_SPEC_CHAR +G_IS_PARAM_SPEC_CLASS +G_IS_PARAM_SPEC_DOUBLE +G_IS_PARAM_SPEC_ENUM +G_IS_PARAM_SPEC_FLAGS +G_IS_PARAM_SPEC_FLOAT +G_IS_PARAM_SPEC_GTYPE +G_IS_PARAM_SPEC_INT +G_IS_PARAM_SPEC_INT64 +G_IS_PARAM_SPEC_LONG +G_IS_PARAM_SPEC_OBJECT +G_IS_PARAM_SPEC_OVERRIDE +G_IS_PARAM_SPEC_PARAM +G_IS_PARAM_SPEC_POINTER +G_IS_PARAM_SPEC_STRING +G_IS_PARAM_SPEC_UCHAR +G_IS_PARAM_SPEC_UINT +G_IS_PARAM_SPEC_UINT64 +G_IS_PARAM_SPEC_ULONG +G_IS_PARAM_SPEC_UNICHAR +G_IS_PARAM_SPEC_VALUE_ARRAY +G_IS_PARAM_SPEC_VARIANT +G_IS_VALUE +Git +GKeyFile +G_KEY_FILE_DESKTOP_GROUP +G_KEY_FILE_DESKTOP_KEY_CATEGORIES +G_KEY_FILE_DESKTOP_KEY_COMMENT +G_KEY_FILE_DESKTOP_KEY_EXEC +G_KEY_FILE_DESKTOP_KEY_GENERIC_NAME +G_KEY_FILE_DESKTOP_KEY_HIDDEN +G_KEY_FILE_DESKTOP_KEY_ICON +G_KEY_FILE_DESKTOP_KEY_MIME_TYPE +G_KEY_FILE_DESKTOP_KEY_NAME +G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY +G_KEY_FILE_DESKTOP_KEY_NOT_SHOW_IN +G_KEY_FILE_DESKTOP_KEY_ONLY_SHOW_IN +G_KEY_FILE_DESKTOP_KEY_PATH +G_KEY_FILE_DESKTOP_KEY_STARTUP_NOTIFY +G_KEY_FILE_DESKTOP_KEY_STARTUP_WM_CLASS +G_KEY_FILE_DESKTOP_KEY_TERMINAL +G_KEY_FILE_DESKTOP_KEY_TRY_EXEC +G_KEY_FILE_DESKTOP_KEY_TYPE +G_KEY_FILE_DESKTOP_KEY_URL +G_KEY_FILE_DESKTOP_KEY_VERSION +G_KEY_FILE_DESKTOP_TYPE_APPLICATION +G_KEY_FILE_DESKTOP_TYPE_DIRECTORY +G_KEY_FILE_DESKTOP_TYPE_LINK +G_KEY_FILE_ERROR +g_key_file_free +g_key_file_get_boolean +g_key_file_get_boolean_list +g_key_file_get_comment +g_key_file_get_double +g_key_file_get_double_list +g_key_file_get_groups +g_key_file_get_int64 +g_key_file_get_integer +g_key_file_get_integer_list +g_key_file_get_keys +g_key_file_get_locale_string +g_key_file_get_locale_string_list +g_key_file_get_start_group +g_key_file_get_string +g_key_file_get_string_list +g_key_file_get_uint64 +g_key_file_get_value +g_key_file_has_group +g_key_file_has_key +g_key_file_load_from_data +g_key_file_load_from_data_dirs +g_key_file_load_from_dirs +g_key_file_load_from_file +g_key_file_new +g_key_file_ref +g_key_file_remove_comment +g_key_file_remove_group +g_key_file_remove_key +g_key_file_set_boolean +g_key_file_set_boolean_list +g_key_file_set_comment +g_key_file_set_double +g_key_file_set_double_list +g_key_file_set_int64 +g_key_file_set_integer +g_key_file_set_integer_list +g_key_file_set_list_separator +g_key_file_set_locale_string +g_key_file_set_locale_string_list +g_key_file_set_string +g_key_file_set_string_list +g_keyfile_settings_backend_new +g_key_file_set_uint64 +g_key_file_set_value +g_key_file_to_data +g_key_file_unref +glBeginQueryIndexed,_glEndQueryIndexed +glColorMask,_glColorMaski +glib_check_version +GLIB_CHECK_VERSION +GLIB_DISABLE_DEPRECATION_WARNINGS +GLIB_MAJOR_VERSION +GLIB_MICRO_VERSION +GLIB_MINOR_VERSION +GLIB_VERSION_2_26 +GLIB_VERSION_2_28 +GLIB_VERSION_2_30 +GLIB_VERSION_2_32 +GLIB_VERSION_2_34 +GLIB_VERSION_2_36 +GLIB_VERSION_MAX_ALLOWED +GLIB_VERSION_MIN_REQUIRED +G_LIKELY +glIsEnabled,_glIsEnabledi +GList +g_list_alloc +g_list_append +g_list_concat +g_list_copy +g_list_copy_deep +g_list_delete_link +g_listenv +g_list_find +g_list_find_custom +g_list_first +g_list_foreach +g_list_free +g_list_free1 +g_list_free_1 +g_list_free_full +g_list_index +g_list_insert +g_list_insert_before +g_list_insert_sorted +g_list_insert_sorted_with_data +g_list_last +g_list_length +g_list_next +g_list_nth +g_list_nth_data +g_list_nth_prev +g_list_position +g_list_prepend +g_list_previous +g_list_remove +g_list_remove_all +g_list_remove_link +g_list_reverse +g_list_sort +g_list_sort_with_data +G_LITTLE_ENDIAN +G_LN10 +G_LN2 +GLoadableIconIface +g_loadable_icon_load +g_loadable_icon_load_async +g_loadable_icon_load_finish +GLoadableIcon +g_locale_from_utf8 +g_locale_to_utf8 +G_LOCK +G_LOCK_DEFINE +G_LOCK_DEFINE_STATIC +G_LOCK_EXTERN +g_log +G_LOG_2_BASE_10 +g_log_default_handler +G_LOG_DOMAIN +G_LOG_FATAL_MASK +GLogFunc +G_LOG_LEVEL_USER_SHIFT +g_log_remove_handler +g_log_set_always_fatal +g_log_set_default_handler +g_log_set_fatal_mask +g_log_set_handler +g_logv +GLONG_FROM_BE +GLONG_FROM_LE +GLONG_TO_BE +GLONG_TO_LE +g_lstat +GMainContext +g_main_context_acquire +g_main_context_add_poll +g_main_context_check +g_main_context_default +g_main_context_dispatch +g_main_context_find_source_by_funcs_user_data +g_main_context_find_source_by_id +g_main_context_find_source_by_user_data +g_main_context_get_poll_func +g_main_context_get_thread_default +g_main_context_invoke +g_main_context_invoke_full +g_main_context_is_owner +g_main_context_iteration +g_main_context_new +g_main_context_pending +g_main_context_pop_thread_default +g_main_context_prepare +g_main_context_push_thread_default +g_main_context_query +g_main_context_ref +g_main_context_ref_thread_default +g_main_context_release +g_main_context_remove_poll +g_main_context_set_poll_func +g_main_context_unref +g_main_context_wait +g_main_context_wakeup +g_main_current_source +g_main_depth +g_main_destroy +g_main_is_running +g_main_iteration +GMainLoop +g_main_loop_get_context +g_main_loop_is_running +g_main_loop_new +g_main_loop_quit +g_main_loop_ref +g_main_loop_run +g_main_loop_unref +g_main_new +g_main_pending +g_main_quit +g_main_run +g_main_set_poll_func +g_malloc +g_malloc0 +g_malloc0_n +g_malloc_n +GMappedFile +g_mapped_file_free +g_mapped_file_get_bytes +g_mapped_file_get_contents +g_mapped_file_get_length +g_mapped_file_new +g_mapped_file_new_from_fd +g_mapped_file_ref +g_mapped_file_unref +g_markup_collect_attributes +G_MARKUP_ERROR +g_markup_escape_text +GMarkupParseContext +g_markup_parse_context_end_parse +g_markup_parse_context_free +g_markup_parse_context_get_element +g_markup_parse_context_get_element_stack +g_markup_parse_context_get_position +g_markup_parse_context_get_user_data +g_markup_parse_context_new +g_markup_parse_context_parse +g_markup_parse_context_pop +g_markup_parse_context_push +GMarkupParser +g_markup_printf_escaped +g_markup_vprintf_escaped +GMatchInfo +g_match_info_expand_references +g_match_info_fetch +g_match_info_fetch_all +g_match_info_fetch_named +g_match_info_fetch_named_pos +g_match_info_fetch_pos +g_match_info_free +g_match_info_get_match_count +g_match_info_get_regex +g_match_info_get_string +g_match_info_is_partial_match +g_match_info_matches +g_match_info_next +g_match_info_ref +g_match_info_unref +G_MAXDOUBLE +G_MAXFLOAT +G_MAXINT +G_MAXINT16 +G_MAXINT32 +G_MAXINT64 +G_MAXINT8 +G_MAXLONG +G_MAXOFFSET +G_MAXSHORT +G_MAXSIZE +G_MAXSSIZE +G_MAXUINT +G_MAXUINT16 +G_MAXUINT32 +G_MAXUINT64 +G_MAXUINT8 +G_MAXULONG +G_MAXUSHORT +G_MEM_ALIGN +g_memdup +g_mem_is_system_malloc +g_memmove +g_memory_input_stream_add_data +g_memory_input_stream_new +g_memory_input_stream_new_from_data +GMemoryInputStream +g_memory_output_stream_get_data +g_memory_output_stream_get_data_size +g_memory_output_stream_get_size +g_memory_output_stream_new +g_memory_output_stream_new_resizable +g_memory_output_stream_steal_data +GMemoryOutputStream +g_memory_settings_backend_new +g_mem_profile +g_mem_set_vtable +GMemVTable +g_menu_append +g_menu_append_item +g_menu_append_section +g_menu_append_submenu +G_MENU_ATTRIBUTE_ACTION +g_menu_attribute_iter_get_name +g_menu_attribute_iter_get_next +g_menu_attribute_iter_get_value +g_menu_attribute_iter_next +GMenuAttributeIter +G_MENU_ATTRIBUTE_LABEL +G_MENU_ATTRIBUTE_TARGET +g_menu_freeze +g_menu_insert +g_menu_insert_item +g_menu_insert_section +g_menu_insert_submenu +g_menu_item_get_attribute +g_menu_item_get_attribute_value +g_menu_item_get_link +g_menu_item_new +g_menu_item_new_from_model +g_menu_item_new_section +g_menu_item_new_submenu +g_menu_item_set_action_and_target +g_menu_item_set_action_and_target_value +g_menu_item_set_attribute +g_menu_item_set_attribute_value +g_menu_item_set_detailed_action +g_menu_item_set_label +g_menu_item_set_link +g_menu_item_set_section +g_menu_item_set_submenu +GMenuItem +g_menu_link_iter_get_name +g_menu_link_iter_get_next +g_menu_link_iter_get_value +g_menu_link_iter_next +GMenuLinkIter +G_MENU_LINK_SECTION +G_MENU_LINK_SUBMENU +g_menu_model_get_item_attribute +g_menu_model_get_item_attribute_value +g_menu_model_get_item_link +g_menu_model_get_n_items +g_menu_model_is_mutable +g_menu_model_items_changed +g_menu_model_iterate_item_attributes +g_menu_model_iterate_item_links +GMenuModel +g_menu_new +g_menu_prepend +g_menu_prepend_item +g_menu_prepend_section +g_menu_prepend_submenu +g_menu_remove +GMenu +g_message +G_MINDOUBLE +G_MINFLOAT +G_MININT +G_MININT16 +G_MININT32 +G_MININT64 +G_MININT8 +G_MINLONG +G_MINOFFSET +G_MINSHORT +G_MINSSIZE +g_mkdir +g_mkdir_with_parents +g_mkdtemp +g_mkdtemp_full +g_mkstemp +g_mkstemp_full +GModule +g_module_build_path +GModuleCheckInit +g_module_close +g_module_error +G_MODULE_EXPORT +G_MODULE_IMPORT +g_module_make_resident +g_module_name +g_module_open +G_MODULE_SUFFIX +g_module_supported +g_module_symbol +GModuleUnload +g_mount_can_eject +g_mount_can_unmount +g_mount_eject +g_mount_eject_finish +g_mount_eject_with_operation +g_mount_eject_with_operation_finish +g_mount_get_default_location +g_mount_get_drive +g_mount_get_icon +g_mount_get_name +g_mount_get_root +g_mount_get_sort_key +g_mount_get_symbolic_icon +g_mount_get_uuid +g_mount_get_volume +g_mount_guess_content_type +g_mount_guess_content_type_finish +g_mount_guess_content_type_sync +GMountIface +g_mount_is_shadowed +g_mount_operation_get_anonymous +g_mount_operation_get_choice +g_mount_operation_get_domain +g_mount_operation_get_password +g_mount_operation_get_password_save +g_mount_operation_get_username +g_mount_operation_new +g_mount_operation_reply +g_mount_operation_set_anonymous +g_mount_operation_set_choice +g_mount_operation_set_domain +g_mount_operation_set_password +g_mount_operation_set_password_save +g_mount_operation_set_username +GMountOperation +g_mount_remount +g_mount_remount_finish +g_mount_shadow +GMount +g_mount_unmount +g_mount_unmount_finish +g_mount_unmount_with_operation +g_mount_unmount_with_operation_finish +g_mount_unshadow +gmtime_r +g_mutex_clear +g_mutex_free +g_mutex_init +g_mutex_lock +g_mutex_new +g_mutex_trylock +g_mutex_unlock +G_N_ELEMENTS +g_network_address_get_hostname +g_network_address_get_port +g_network_address_get_scheme +g_network_address_new +g_network_address_parse +g_network_address_parse_uri +GNetworkAddress +g_networking_init +g_network_monitor_can_reach +g_network_monitor_can_reach_async +g_network_monitor_can_reach_finish +G_NETWORK_MONITOR_EXTENSION_POINT_NAME +g_network_monitor_get_default +g_network_monitor_get_network_available +GNetworkMonitorInterface +GNetworkMonitor +g_network_service_get_domain +g_network_service_get_protocol +g_network_service_get_scheme +g_network_service_get_service +g_network_service_new +g_network_service_set_scheme +GNetworkService +g_new +g_new0 +g_newa +GNode +g_node_append +g_node_append_data +g_node_child_index +g_node_child_position +g_node_children_foreach +g_node_copy +g_node_copy_deep +g_node_depth +g_node_destroy +g_node_find +g_node_find_child +g_node_first_child +g_node_first_sibling +GNodeForeachFunc +g_node_get_root +g_node_insert +g_node_insert_after +g_node_insert_before +g_node_insert_data +g_node_insert_data_after +g_node_insert_data_before +g_node_is_ancestor +G_NODE_IS_LEAF +G_NODE_IS_ROOT +g_node_last_child +g_node_last_sibling +g_node_max_height +g_node_n_children +g_node_new +g_node_next_sibling +g_node_n_nodes +g_node_nth_child +g_node_prepend +g_node_prepend_data +g_node_prev_sibling +g_node_reverse_children +g_node_traverse +GNodeTraverseFunc +g_node_unlink +g_ntohl +g_ntohs +gnu_dev_major +gnu_dev_makedev +gnu_dev_minor +gnu_get_libc_release +gnu_get_libc_version +g_nullify_pointer +g_null_settings_backend_new +gnutls_alert_get +gnutls_alert_get_name +gnutls_alert_send +gnutls_alert_send_appropriate +gnutls_anon_allocate_client_credentials +gnutls_anon_allocate_server_credentials +gnutls_anon_free_client_credentials +gnutls_anon_free_server_credentials +gnutls_anon_set_params_function +gnutls_anon_set_server_dh_params +gnutls_anon_set_server_params_function +gnutls_auth_client_get_type +gnutls_auth_get_type +gnutls_auth_server_get_type +gnutls_bye +gnutls_certificate_activation_time_peers +gnutls_certificate_allocate_credentials +gnutls_certificate_client_get_request_status +gnutls_certificate_client_set_retrieve_function +gnutls_certificate_expiration_time_peers +gnutls_certificate_free_ca_names +gnutls_certificate_free_cas +gnutls_certificate_free_credentials +gnutls_certificate_free_crls +gnutls_certificate_free_keys +gnutls_certificate_get_issuer +gnutls_certificate_get_openpgp_keyring +gnutls_certificate_get_ours +gnutls_certificate_get_peers +gnutls_certificate_get_x509_cas +gnutls_certificate_get_x509_crls +gnutls_certificate_send_x509_rdn_sequence +gnutls_certificate_server_set_request +gnutls_certificate_server_set_retrieve_function +gnutls_certificate_set_dh_params +gnutls_certificate_set_openpgp_key +gnutls_certificate_set_openpgp_key_file +gnutls_certificate_set_openpgp_key_file2 +gnutls_certificate_set_openpgp_key_mem +gnutls_certificate_set_openpgp_key_mem2 +gnutls_certificate_set_openpgp_keyring_file +gnutls_certificate_set_openpgp_keyring_mem +gnutls_certificate_set_params_function +gnutls_certificate_set_retrieve_function +gnutls_certificate_set_rsa_export_params +gnutls_certificate_set_verify_flags +gnutls_certificate_set_verify_function +gnutls_certificate_set_verify_limits +gnutls_certificate_set_x509_crl +gnutls_certificate_set_x509_crl_file +gnutls_certificate_set_x509_crl_mem +gnutls_certificate_set_x509_key +gnutls_certificate_set_x509_key_file +gnutls_certificate_set_x509_key_mem +gnutls_certificate_set_x509_simple_pkcs12_file +gnutls_certificate_set_x509_simple_pkcs12_mem +gnutls_certificate_set_x509_trust +gnutls_certificate_set_x509_trust_file +gnutls_certificate_set_x509_trust_mem +gnutls_certificate_type_get +gnutls_certificate_type_get_id +gnutls_certificate_type_get_name +gnutls_certificate_type_list +gnutls_certificate_type_set_priority +gnutls_certificate_verify_peers +gnutls_certificate_verify_peers2 +gnutls_check_version +gnutls_cipher_decrypt +gnutls_cipher_decrypt2 +gnutls_cipher_deinit +gnutls_cipher_encrypt +gnutls_cipher_encrypt2 +gnutls_cipher_get +gnutls_cipher_get_block_size +gnutls_cipher_get_id +gnutls_cipher_get_key_size +gnutls_cipher_get_name +gnutls_cipher_init +gnutls_cipher_list +gnutls_cipher_set_priority +gnutls_cipher_suite_get_name +gnutls_cipher_suite_info +gnutls_compression_get +gnutls_compression_get_id +gnutls_compression_get_name +gnutls_compression_list +gnutls_compression_set_priority +gnutls_credentials_clear +gnutls_credentials_set +gnutls_crypto_bigint_register2 +gnutls_crypto_cipher_register2 +gnutls_crypto_digest_register2 +gnutls_crypto_mac_register2 +gnutls_crypto_pk_register2 +gnutls_crypto_rnd_register2 +gnutls_crypto_single_cipher_register2 +gnutls_crypto_single_digest_register2 +gnutls_crypto_single_mac_register2 +gnutls_db_check_entry +gnutls_db_get_ptr +gnutls_db_remove_session +gnutls_db_set_cache_expiration +gnutls_db_set_ptr +gnutls_db_set_remove_function +gnutls_db_set_retrieve_function +gnutls_db_set_store_function +gnutls_deinit +gnutls_dh_get_group +gnutls_dh_get_peers_public_bits +gnutls_dh_get_prime_bits +gnutls_dh_get_pubkey +gnutls_dh_get_secret_bits +gnutls_dh_params_cpy +gnutls_dh_params_deinit +gnutls_dh_params_export_pkcs3 +gnutls_dh_params_export_raw +gnutls_dh_params_generate2 +gnutls_dh_params_import_pkcs3 +gnutls_dh_params_import_raw +gnutls_dh_params_init +gnutls_dh_set_prime_bits +gnutls_error_is_fatal +gnutls_error_to_alert +gnutls_extra_check_version +gnutls_ext_register +gnutls_fingerprint +gnutls_free +gnutls_global_deinit +gnutls_global_init +gnutls_global_init_extra +gnutls_global_set_log_function +gnutls_global_set_log_level +gnutls_global_set_mem_functions +gnutls_global_set_mutex +gnutls_global_set_time_function +gnutls_handshake +gnutls_handshake_get_last_in +gnutls_handshake_get_last_out +gnutls_handshake_set_max_packet_length +gnutls_handshake_set_post_client_hello_function +gnutls_handshake_set_private_extensions +gnutls_hash +gnutls_hash_deinit +gnutls_hash_fast +gnutls_hash_get_len +gnutls_hash_init +gnutls_hash_output +gnutls_hex2bin +gnutls_hex_decode +gnutls_hex_encode +gnutls_hmac +gnutls_hmac_deinit +gnutls_hmac_fast +gnutls_hmac_get_len +gnutls_hmac_init +gnutls_hmac_output +gnutls_ia_allocate_client_credentials +gnutls_ia_allocate_server_credentials +gnutls_ia_enable +gnutls_ia_endphase_send +gnutls_ia_extract_inner_secret +gnutls_ia_free_client_credentials +gnutls_ia_free_server_credentials +gnutls_ia_generate_challenge +gnutls_ia_get_client_avp_ptr +gnutls_ia_get_server_avp_ptr +gnutls_ia_handshake +gnutls_ia_handshake_p +gnutls_ia_permute_inner_secret +gnutls_ia_recv +gnutls_ia_send +gnutls_ia_set_client_avp_function +gnutls_ia_set_client_avp_ptr +gnutls_ia_set_server_avp_function +gnutls_ia_set_server_avp_ptr +gnutls_ia_verify_endphase +gnutls_init +gnutls_kx_get +gnutls_kx_get_id +gnutls_kx_get_name +gnutls_kx_list +gnutls_kx_set_priority +gnutls_mac_get +gnutls_mac_get_id +gnutls_mac_get_key_size +gnutls_mac_get_name +gnutls_mac_list +gnutls_mac_set_priority +gnutls_malloc +gnutls_openpgp_crt_check_hostname +gnutls_openpgp_crt_deinit +gnutls_openpgp_crt_export +gnutls_openpgp_crt_get_auth_subkey +gnutls_openpgp_crt_get_creation_time +gnutls_openpgp_crt_get_expiration_time +gnutls_openpgp_crt_get_fingerprint +gnutls_openpgp_crt_get_key_id +gnutls_openpgp_crt_get_key_usage +gnutls_openpgp_crt_get_name +gnutls_openpgp_crt_get_pk_algorithm +gnutls_openpgp_crt_get_pk_dsa_raw +gnutls_openpgp_crt_get_pk_rsa_raw +gnutls_openpgp_crt_get_preferred_key_id +gnutls_openpgp_crt_get_revoked_status +gnutls_openpgp_crt_get_subkey_count +gnutls_openpgp_crt_get_subkey_creation_time +gnutls_openpgp_crt_get_subkey_expiration_time +gnutls_openpgp_crt_get_subkey_fingerprint +gnutls_openpgp_crt_get_subkey_id +gnutls_openpgp_crt_get_subkey_idx +gnutls_openpgp_crt_get_subkey_pk_algorithm +gnutls_openpgp_crt_get_subkey_pk_dsa_raw +gnutls_openpgp_crt_get_subkey_pk_rsa_raw +gnutls_openpgp_crt_get_subkey_revoked_status +gnutls_openpgp_crt_get_subkey_usage +gnutls_openpgp_crt_get_version +gnutls_openpgp_crt_import +gnutls_openpgp_crt_init +gnutls_openpgp_crt_print +gnutls_openpgp_crt_set_preferred_key_id +gnutls_openpgp_crt_verify_ring +gnutls_openpgp_crt_verify_self +gnutls_openpgp_keyring_check_id +gnutls_openpgp_keyring_deinit +gnutls_openpgp_keyring_get_crt +gnutls_openpgp_keyring_get_crt_count +gnutls_openpgp_keyring_import +gnutls_openpgp_keyring_init +gnutls_openpgp_privkey_deinit +gnutls_openpgp_privkey_export +gnutls_openpgp_privkey_export_dsa_raw +gnutls_openpgp_privkey_export_rsa_raw +gnutls_openpgp_privkey_export_subkey_dsa_raw +gnutls_openpgp_privkey_export_subkey_rsa_raw +gnutls_openpgp_privkey_get_fingerprint +gnutls_openpgp_privkey_get_key_id +gnutls_openpgp_privkey_get_pk_algorithm +gnutls_openpgp_privkey_get_preferred_key_id +gnutls_openpgp_privkey_get_revoked_status +gnutls_openpgp_privkey_get_subkey_count +gnutls_openpgp_privkey_get_subkey_creation_time +gnutls_openpgp_privkey_get_subkey_expiration_time +gnutls_openpgp_privkey_get_subkey_fingerprint +gnutls_openpgp_privkey_get_subkey_id +gnutls_openpgp_privkey_get_subkey_idx +gnutls_openpgp_privkey_get_subkey_pk_algorithm +gnutls_openpgp_privkey_get_subkey_revoked_status +gnutls_openpgp_privkey_import +gnutls_openpgp_privkey_init +gnutls_openpgp_privkey_sec_param +gnutls_openpgp_privkey_set_preferred_key_id +gnutls_openpgp_privkey_sign_hash +gnutls_openpgp_send_cert +gnutls_openpgp_set_recv_key_function +gnutls_pem_base64_decode +gnutls_pem_base64_decode_alloc +gnutls_pem_base64_encode +gnutls_pem_base64_encode_alloc +gnutls_perror +gnutls_pk_algorithm_get_name +gnutls_pk_bits_to_sec_param +gnutls_pkcs11_add_provider +gnutls_pkcs11_copy_secret_key +gnutls_pkcs11_copy_x509_crt +gnutls_pkcs11_copy_x509_privkey +gnutls_pkcs11_deinit +gnutls_pkcs11_delete_url +gnutls_pkcs11_init +gnutls_pkcs11_obj_deinit +gnutls_pkcs11_obj_export +gnutls_pkcs11_obj_export_url +gnutls_pkcs11_obj_get_info +gnutls_pkcs11_obj_get_type +gnutls_pkcs11_obj_import_url +gnutls_pkcs11_obj_init +gnutls_pkcs11_obj_list_import_url +gnutls_pkcs11_privkey_deinit +gnutls_pkcs11_privkey_export_url +gnutls_pkcs11_privkey_get_info +gnutls_pkcs11_privkey_get_pk_algorithm +gnutls_pkcs11_privkey_import_url +gnutls_pkcs11_privkey_init +gnutls_pkcs11_set_pin_function +gnutls_pkcs11_set_token_function +gnutls_pkcs11_token_get_flags +gnutls_pkcs11_token_get_info +gnutls_pkcs11_token_get_mechanism +gnutls_pkcs11_token_get_url +gnutls_pkcs11_token_init +gnutls_pkcs11_token_set_pin +gnutls_pkcs12_bag_decrypt +gnutls_pkcs12_bag_deinit +gnutls_pkcs12_bag_encrypt +gnutls_pkcs12_bag_get_count +gnutls_pkcs12_bag_get_data +gnutls_pkcs12_bag_get_friendly_name +gnutls_pkcs12_bag_get_key_id +gnutls_pkcs12_bag_get_type +gnutls_pkcs12_bag_init +gnutls_pkcs12_bag_set_crl +gnutls_pkcs12_bag_set_crt +gnutls_pkcs12_bag_set_data +gnutls_pkcs12_bag_set_friendly_name +gnutls_pkcs12_bag_set_key_id +gnutls_pkcs12_deinit +gnutls_pkcs12_export +gnutls_pkcs12_generate_mac +gnutls_pkcs12_get_bag +gnutls_pkcs12_import +gnutls_pkcs12_init +gnutls_pkcs12_set_bag +gnutls_pkcs12_verify_mac +gnutls_pkcs7_deinit +gnutls_pkcs7_delete_crl +gnutls_pkcs7_delete_crt +gnutls_pkcs7_export +gnutls_pkcs7_get_crl_count +gnutls_pkcs7_get_crl_raw +gnutls_pkcs7_get_crt_count +gnutls_pkcs7_get_crt_raw +gnutls_pkcs7_import +gnutls_pkcs7_init +gnutls_pkcs7_set_crl +gnutls_pkcs7_set_crl_raw +gnutls_pkcs7_set_crt +gnutls_pkcs7_set_crt_raw +gnutls_pk_get_id +gnutls_pk_get_name +gnutls_pk_list +gnutls_prf +gnutls_prf_raw +gnutls_priority_deinit +gnutls_priority_init +gnutls_priority_set +gnutls_priority_set_direct +gnutls_privkey_decrypt_data +gnutls_privkey_deinit +gnutls_privkey_get_pk_algorithm +gnutls_privkey_get_type +gnutls_privkey_import_openpgp +gnutls_privkey_import_pkcs11 +gnutls_privkey_import_x509 +gnutls_privkey_init +gnutls_privkey_sign_data +gnutls_privkey_sign_hash +gnutls_protocol_get_id +gnutls_protocol_get_name +gnutls_protocol_get_version +gnutls_protocol_list +gnutls_protocol_set_priority +gnutls_psk_allocate_client_credentials +gnutls_psk_allocate_server_credentials +gnutls_psk_client_get_hint +gnutls_psk_free_client_credentials +gnutls_psk_free_server_credentials +gnutls_psk_netconf_derive_key +gnutls_psk_server_get_username +gnutls_psk_set_client_credentials +gnutls_psk_set_client_credentials_function +gnutls_psk_set_params_function +gnutls_psk_set_server_credentials_file +gnutls_psk_set_server_credentials_function +gnutls_psk_set_server_credentials_hint +gnutls_psk_set_server_dh_params +gnutls_psk_set_server_params_function +gnutls_pubkey_deinit +gnutls_pubkey_export +gnutls_pubkey_get_key_id +gnutls_pubkey_get_key_usage +gnutls_pubkey_get_pk_algorithm +gnutls_pubkey_get_pk_dsa_raw +gnutls_pubkey_get_pk_rsa_raw +gnutls_pubkey_get_preferred_hash_algorithm +gnutls_pubkey_get_verify_algorithm +gnutls_pubkey_import +gnutls_pubkey_import_dsa_raw +gnutls_pubkey_import_openpgp +gnutls_pubkey_import_pkcs11 +gnutls_pubkey_import_pkcs11_url +gnutls_pubkey_import_privkey +gnutls_pubkey_import_rsa_raw +gnutls_pubkey_import_x509 +gnutls_pubkey_init +gnutls_pubkey_set_key_usage +gnutls_pubkey_verify_data +gnutls_pubkey_verify_hash +gnutls_record_check_pending +gnutls_record_disable_padding +gnutls_record_get_direction +gnutls_record_get_max_size +gnutls_record_recv +gnutls_record_send +gnutls_record_set_max_size +gnutls_register_md5_handler +gnutls_rehandshake +gnutls_rnd +gnutls_rsa_export_get_modulus_bits +gnutls_rsa_export_get_pubkey +gnutls_rsa_params_cpy +gnutls_rsa_params_deinit +gnutls_rsa_params_export_pkcs1 +gnutls_rsa_params_export_raw +gnutls_rsa_params_generate2 +gnutls_rsa_params_import_pkcs1 +gnutls_rsa_params_import_raw +gnutls_rsa_params_init +gnutls_safe_renegotiation_status +gnutls_sec_param_get_name +gnutls_sec_param_to_pk_bits +gnutls_server_name_get +gnutls_server_name_set +gnutls_session_channel_binding +gnutls_session_enable_compatibility_mode +gnutls_session_get_data +gnutls_session_get_data2 +gnutls_session_get_id +gnutls_session_get_ptr +gnutls_session_is_resumed +gnutls_session_set_data +gnutls_session_set_ptr +gnutls_session_ticket_enable_client +gnutls_session_ticket_enable_server +gnutls_session_ticket_key_generate +gnutls_set_default_export_priority +gnutls_set_default_priority +gnutls_sign_algorithm_get_name +gnutls_sign_algorithm_get_requested +gnutls_sign_callback_get +gnutls_sign_callback_set +gnutls_sign_get_id +gnutls_sign_get_name +gnutls_sign_list +gnutls_strerror +gnutls_strerror_name +gnutls_supplemental_get_name +gnutls_transport_get_ptr +gnutls_transport_get_ptr2 +gnutls_transport_set_errno +gnutls_transport_set_errno_function +gnutls_transport_set_global_errno +gnutls_transport_set_lowat +gnutls_transport_set_ptr +gnutls_transport_set_ptr2 +gnutls_transport_set_pull_function +gnutls_transport_set_push_function +gnutls_transport_set_vec_push_function +gnutls_x509_crl_check_issuer +gnutls_x509_crl_deinit +gnutls_x509_crl_export +gnutls_x509_crl_get_authority_key_id +gnutls_x509_crl_get_crt_count +gnutls_x509_crl_get_crt_serial +gnutls_x509_crl_get_dn_oid +gnutls_x509_crl_get_extension_data +gnutls_x509_crl_get_extension_info +gnutls_x509_crl_get_extension_oid +gnutls_x509_crl_get_issuer_dn +gnutls_x509_crl_get_issuer_dn_by_oid +gnutls_x509_crl_get_next_update +gnutls_x509_crl_get_number +gnutls_x509_crl_get_raw_issuer_dn +gnutls_x509_crl_get_signature +gnutls_x509_crl_get_signature_algorithm +gnutls_x509_crl_get_this_update +gnutls_x509_crl_get_version +gnutls_x509_crl_import +gnutls_x509_crl_init +gnutls_x509_crl_print +gnutls_x509_crl_privkey_sign +gnutls_x509_crl_set_authority_key_id +gnutls_x509_crl_set_crt +gnutls_x509_crl_set_crt_serial +gnutls_x509_crl_set_next_update +gnutls_x509_crl_set_number +gnutls_x509_crl_set_this_update +gnutls_x509_crl_set_version +gnutls_x509_crl_sign +gnutls_x509_crl_sign2 +gnutls_x509_crl_verify +gnutls_x509_crq_deinit +gnutls_x509_crq_export +gnutls_x509_crq_get_attribute_by_oid +gnutls_x509_crq_get_attribute_data +gnutls_x509_crq_get_attribute_info +gnutls_x509_crq_get_basic_constraints +gnutls_x509_crq_get_challenge_password +gnutls_x509_crq_get_dn +gnutls_x509_crq_get_dn_by_oid +gnutls_x509_crq_get_dn_oid +gnutls_x509_crq_get_extension_by_oid +gnutls_x509_crq_get_extension_data +gnutls_x509_crq_get_extension_info +gnutls_x509_crq_get_key_id +gnutls_x509_crq_get_key_purpose_oid +gnutls_x509_crq_get_key_rsa_raw +gnutls_x509_crq_get_key_usage +gnutls_x509_crq_get_pk_algorithm +gnutls_x509_crq_get_subject_alt_name +gnutls_x509_crq_get_subject_alt_othername_oid +gnutls_x509_crq_get_version +gnutls_x509_crq_import +gnutls_x509_crq_init +gnutls_x509_crq_print +gnutls_x509_crq_privkey_sign +gnutls_x509_crq_set_attribute_by_oid +gnutls_x509_crq_set_basic_constraints +gnutls_x509_crq_set_challenge_password +gnutls_x509_crq_set_dn_by_oid +gnutls_x509_crq_set_key +gnutls_x509_crq_set_key_purpose_oid +gnutls_x509_crq_set_key_rsa_raw +gnutls_x509_crq_set_key_usage +gnutls_x509_crq_set_pubkey +gnutls_x509_crq_set_subject_alt_name +gnutls_x509_crq_set_version +gnutls_x509_crq_sign +gnutls_x509_crq_sign2 +gnutls_x509_crq_verify +gnutls_x509_crt_check_hostname +gnutls_x509_crt_check_issuer +gnutls_x509_crt_check_revocation +gnutls_x509_crt_cpy_crl_dist_points +gnutls_x509_crt_deinit +gnutls_x509_crt_export +gnutls_x509_crt_get_activation_time +gnutls_x509_crt_get_authority_key_id +gnutls_x509_crt_get_basic_constraints +gnutls_x509_crt_get_ca_status +gnutls_x509_crt_get_crl_dist_points +gnutls_x509_crt_get_dn +gnutls_x509_crt_get_dn_by_oid +gnutls_x509_crt_get_dn_oid +gnutls_x509_crt_get_expiration_time +gnutls_x509_crt_get_extension_by_oid +gnutls_x509_crt_get_extension_data +gnutls_x509_crt_get_extension_info +gnutls_x509_crt_get_extension_oid +gnutls_x509_crt_get_fingerprint +gnutls_x509_crt_get_issuer +gnutls_x509_crt_get_issuer_alt_name +gnutls_x509_crt_get_issuer_alt_name2 +gnutls_x509_crt_get_issuer_alt_othername_oid +gnutls_x509_crt_get_issuer_dn +gnutls_x509_crt_get_issuer_dn_by_oid +gnutls_x509_crt_get_issuer_dn_oid +gnutls_x509_crt_get_issuer_unique_id +gnutls_x509_crt_get_key_id +gnutls_x509_crt_get_key_purpose_oid +gnutls_x509_crt_get_key_usage +gnutls_x509_crt_get_pk_algorithm +gnutls_x509_crt_get_pk_dsa_raw +gnutls_x509_crt_get_pk_rsa_raw +gnutls_x509_crt_get_preferred_hash_algorithm +gnutls_x509_crt_get_proxy +gnutls_x509_crt_get_raw_dn +gnutls_x509_crt_get_raw_issuer_dn +gnutls_x509_crt_get_serial +gnutls_x509_crt_get_signature +gnutls_x509_crt_get_signature_algorithm +gnutls_x509_crt_get_subject +gnutls_x509_crt_get_subject_alt_name +gnutls_x509_crt_get_subject_alt_name2 +gnutls_x509_crt_get_subject_alt_othername_oid +gnutls_x509_crt_get_subject_key_id +gnutls_x509_crt_get_subject_unique_id +gnutls_x509_crt_get_verify_algorithm +gnutls_x509_crt_get_version +gnutls_x509_crt_import +gnutls_x509_crt_import_pkcs11 +gnutls_x509_crt_import_pkcs11_url +gnutls_x509_crt_init +gnutls_x509_crt_list_import +gnutls_x509_crt_list_import_pkcs11 +gnutls_x509_crt_list_verify +gnutls_x509_crt_print +gnutls_x509_crt_privkey_sign +gnutls_x509_crt_set_activation_time +gnutls_x509_crt_set_authority_key_id +gnutls_x509_crt_set_basic_constraints +gnutls_x509_crt_set_ca_status +gnutls_x509_crt_set_crl_dist_points +gnutls_x509_crt_set_crl_dist_points2 +gnutls_x509_crt_set_crq +gnutls_x509_crt_set_crq_extensions +gnutls_x509_crt_set_dn_by_oid +gnutls_x509_crt_set_expiration_time +gnutls_x509_crt_set_extension_by_oid +gnutls_x509_crt_set_issuer_dn_by_oid +gnutls_x509_crt_set_key +gnutls_x509_crt_set_key_purpose_oid +gnutls_x509_crt_set_key_usage +gnutls_x509_crt_set_proxy +gnutls_x509_crt_set_proxy_dn +gnutls_x509_crt_set_pubkey +gnutls_x509_crt_set_serial +gnutls_x509_crt_set_subject_alternative_name +gnutls_x509_crt_set_subject_alt_name +gnutls_x509_crt_set_subject_key_id +gnutls_x509_crt_set_version +gnutls_x509_crt_sign +gnutls_x509_crt_sign2 +gnutls_x509_crt_verify +gnutls_x509_crt_verify_data +gnutls_x509_crt_verify_hash +gnutls_x509_dn_deinit +gnutls_x509_dn_export +gnutls_x509_dn_get_rdn_ava +gnutls_x509_dn_import +gnutls_x509_dn_init +gnutls_x509_dn_oid_known +gnutls_x509_privkey_cpy +gnutls_x509_privkey_deinit +gnutls_x509_privkey_export +gnutls_x509_privkey_export_dsa_raw +gnutls_x509_privkey_export_pkcs8 +gnutls_x509_privkey_export_rsa_raw +gnutls_x509_privkey_export_rsa_raw2 +gnutls_x509_privkey_fix +gnutls_x509_privkey_generate +gnutls_x509_privkey_get_key_id +gnutls_x509_privkey_get_pk_algorithm +gnutls_x509_privkey_import +gnutls_x509_privkey_import_dsa_raw +gnutls_x509_privkey_import_pkcs8 +gnutls_x509_privkey_import_rsa_raw +gnutls_x509_privkey_import_rsa_raw2 +gnutls_x509_privkey_init +gnutls_x509_privkey_sec_param +gnutls_x509_privkey_sign_data +gnutls_x509_privkey_sign_hash +gnutls_x509_privkey_verify_data +gnutls_x509_rdn_get +gnutls_x509_rdn_get_by_oid +gnutls_x509_rdn_get_oid +G_OBJECT +g_object_add_toggle_ref +g_object_add_weak_pointer +g_object_bind_property +g_object_bind_property_full +g_object_bind_property_with_closures +GObjectClass +G_OBJECT_CLASS +g_object_class_find_property +g_object_class_install_properties +g_object_class_install_property +g_object_class_list_properties +G_OBJECT_CLASS_NAME +g_object_class_override_property +G_OBJECT_CLASS_TYPE +g_object_connect +GObjectConstructParam +g_object_disconnect +g_object_dup_data +g_object_dup_qdata +GObjectFinalizeFunc +g_object_force_floating +g_object_freeze_notify +g_object_get +G_OBJECT_GET_CLASS +g_object_get_data +g_object_get_property +GObjectGetPropertyFunc +g_object_get_qdata +g_object_get_valist +g_object_info_find_method +g_object_info_find_vfunc +g_object_info_get_abstract +g_object_info_get_class +g_object_info_get_constant +g_object_info_get_field +g_object_info_get_fundamental +g_object_info_get_get_value_function +g_object_info_get_get_value_function_pointer +g_object_info_get_interface +g_object_info_get_method +g_object_info_get_n_constants +g_object_info_get_n_fields +g_object_info_get_n_interfaces +g_object_info_get_n_methods +g_object_info_get_n_properties +g_object_info_get_n_signals +g_object_info_get_n_vfuncs +g_object_info_get_parent +g_object_info_get_property +g_object_info_get_ref_function +g_object_info_get_ref_function_pointer +g_object_info_get_set_value_function +g_object_info_get_set_value_function_pointer +g_object_info_get_signal +g_object_info_get_type_init +g_object_info_get_type_name +g_object_info_get_unref_function +g_object_info_get_unref_function_pointer +g_object_info_get_vfunc +g_object_interface_find_property +g_object_interface_install_property +g_object_interface_list_properties +g_object_is_floating +g_object_new +g_object_newv +g_object_new_valist +g_object_notify +g_object_notify_by_pspec +g_object_ref +g_object_ref_sink +g_object_remove_toggle_ref +g_object_remove_weak_pointer +g_object_replace_data +g_object_replace_qdata +g_object_run_dispose +g_object_set +g_object_set_data +g_object_set_data_full +g_object_set_property +GObjectSetPropertyFunc +g_object_set_qdata +g_object_set_qdata_full +g_object_set_valist +g_object_steal_data +g_object_steal_qdata +GObject +g_object_thaw_notify +G_OBJECT_TYPE +G_OBJECT_TYPE_NAME +g_object_unref +G_OBJECT_WARN_INVALID_PROPERTY_ID +g_object_watch_closure +g_object_weak_ref +g_object_weak_unref +g_once +GOnce +G_ONCE_INIT +g_once_init_enter +g_once_init_leave +g_on_error_query +g_on_error_stack_trace +g_open +GOptionArgFunc +GOptionContext +g_option_context_add_group +g_option_context_add_main_entries +g_option_context_free +g_option_context_get_description +g_option_context_get_help +g_option_context_get_help_enabled +g_option_context_get_ignore_unknown_options +g_option_context_get_main_group +g_option_context_get_summary +g_option_context_new +g_option_context_parse +g_option_context_set_description +g_option_context_set_help_enabled +g_option_context_set_ignore_unknown_options +g_option_context_set_main_group +g_option_context_set_summary +g_option_context_set_translate_func +g_option_context_set_translation_domain +GOptionEntry +G_OPTION_ERROR +GOptionErrorFunc +GOptionGroup +g_option_group_add_entries +g_option_group_free +g_option_group_new +g_option_group_set_error_hook +g_option_group_set_parse_hooks +g_option_group_set_translate_func +g_option_group_set_translation_domain +GOptionParseFunc +G_OPTION_REMAINING +G_OS_BEOS +G_OS_UNIX +G_OS_WIN32 +g_output_stream_clear_pending +g_output_stream_close +g_output_stream_close_async +g_output_stream_close_finish +g_output_stream_flush +g_output_stream_flush_async +g_output_stream_flush_finish +g_output_stream_has_pending +g_output_stream_is_closed +g_output_stream_is_closing +g_output_stream_set_pending +g_output_stream_splice +g_output_stream_splice_async +g_output_stream_splice_finish +GOutputStream +g_output_stream_write +g_output_stream_write_all +g_output_stream_write_async +g_output_stream_write_bytes +g_output_stream_write_bytes_async +g_output_stream_write_bytes_finish +g_output_stream_write_finish +GOutputVector +GParameter +G_PARAM_MASK +G_PARAM_READWRITE +GParamSpec +G_PARAM_SPEC +g_param_spec_boolean +GParamSpecBoolean +G_PARAM_SPEC_BOOLEAN +g_param_spec_boxed +GParamSpecBoxed +G_PARAM_SPEC_BOXED +g_param_spec_char +GParamSpecChar +G_PARAM_SPEC_CHAR +GParamSpecClass +G_PARAM_SPEC_CLASS +g_param_spec_double +GParamSpecDouble +G_PARAM_SPEC_DOUBLE +g_param_spec_enum +GParamSpecEnum +G_PARAM_SPEC_ENUM +g_param_spec_flags +GParamSpecFlags +G_PARAM_SPEC_FLAGS +g_param_spec_float +GParamSpecFloat +G_PARAM_SPEC_FLOAT +g_param_spec_get_blurb +G_PARAM_SPEC_GET_CLASS +g_param_spec_get_name +g_param_spec_get_nick +g_param_spec_get_qdata +g_param_spec_get_redirect_target +g_param_spec_gtype +GParamSpecGType +G_PARAM_SPEC_GTYPE +g_param_spec_int +GParamSpecInt +G_PARAM_SPEC_INT +g_param_spec_int64 +GParamSpecInt64 +G_PARAM_SPEC_INT64 +g_param_spec_internal +g_param_spec_long +GParamSpecLong +G_PARAM_SPEC_LONG +g_param_spec_object +GParamSpecObject +G_PARAM_SPEC_OBJECT +g_param_spec_override +GParamSpecOverride +G_PARAM_SPEC_OVERRIDE +g_param_spec_param +GParamSpecParam +G_PARAM_SPEC_PARAM +g_param_spec_pointer +GParamSpecPointer +G_PARAM_SPEC_POINTER +GParamSpecPool +g_param_spec_pool_insert +g_param_spec_pool_list +g_param_spec_pool_list_owned +g_param_spec_pool_lookup +g_param_spec_pool_new +g_param_spec_pool_remove +g_param_spec_ref +g_param_spec_ref_sink +g_param_spec_set_qdata +g_param_spec_set_qdata_full +g_param_spec_sink +g_param_spec_steal_qdata +g_param_spec_string +GParamSpecString +G_PARAM_SPEC_STRING +G_PARAM_SPEC_TYPE +GParamSpecTypeInfo +G_PARAM_SPEC_TYPE_NAME +g_param_spec_uchar +GParamSpecUChar +G_PARAM_SPEC_UCHAR +g_param_spec_uint +GParamSpecUInt +G_PARAM_SPEC_UINT +g_param_spec_uint64 +GParamSpecUInt64 +G_PARAM_SPEC_UINT64 +g_param_spec_ulong +GParamSpecULong +G_PARAM_SPEC_ULONG +g_param_spec_unichar +GParamSpecUnichar +G_PARAM_SPEC_UNICHAR +g_param_spec_unref +g_param_spec_value_array +GParamSpecValueArray +G_PARAM_SPEC_VALUE_ARRAY +G_PARAM_SPEC_VALUE_TYPE +g_param_spec_variant +GParamSpecVariant +G_PARAM_SPEC_VARIANT +G_PARAM_STATIC_STRINGS +g_param_type_register_static +G_PARAM_USER_SHIFT +g_param_value_convert +g_param_value_defaults +g_param_values_cmp +g_param_value_set_default +g_param_value_validate +g_parse_debug_string +G_PASTE +g_path_get_basename +g_path_get_dirname +g_path_is_absolute +g_path_skip_root +g_pattern_match +g_pattern_match_simple +g_pattern_match_string +GPatternSpec +g_pattern_spec_equal +g_pattern_spec_free +g_pattern_spec_new +G_PDP_ENDIAN +g_permission_acquire +g_permission_acquire_async +g_permission_acquire_finish +g_permission_get_allowed +g_permission_get_can_acquire +g_permission_get_can_release +g_permission_impl_update +g_permission_release +g_permission_release_async +g_permission_release_finish +GPermission +G_PI +G_PI_2 +G_PI_4 +GPid +g_pointer_bit_lock +g_pointer_bit_trylock +g_pointer_bit_unlock +GPOINTER_TO_INT +GPOINTER_TO_SIZE +GPOINTER_TO_UINT +g_pointer_type_register_static +g_poll +g_pollable_input_stream_can_poll +g_pollable_input_stream_create_source +GPollableInputStreamInterface +g_pollable_input_stream_is_readable +g_pollable_input_stream_read_nonblocking +GPollableInputStream +g_pollable_output_stream_can_poll +g_pollable_output_stream_create_source +GPollableOutputStreamInterface +g_pollable_output_stream_is_writable +GPollableOutputStream +g_pollable_output_stream_write_nonblocking +GPollableSourceFunc +g_pollable_source_new +g_pollable_source_new_full +g_pollable_stream_read +g_pollable_stream_write +g_pollable_stream_write_all +GPollFD +G_POLLFD_FORMAT +GPollFunc +g_prefix_error +g_print +g_printerr +g_printf +g_printf_string_upper_bound +GPrintFunc +G_PRIORITY_DEFAULT +G_PRIORITY_DEFAULT_IDLE +G_PRIORITY_HIGH +G_PRIORITY_HIGH_IDLE +G_PRIORITY_LOW +GPrivate +g_private_get +G_PRIVATE_INIT +g_private_new +g_private_replace +g_private_set +g_propagate_error +g_propagate_prefixed_error +g_property_info_get_flags +g_property_info_get_ownership_transfer +g_property_info_get_type +GProxyAddressClass +GProxyAddressEnumerator +g_proxy_address_get_destination_hostname +g_proxy_address_get_destination_port +g_proxy_address_get_destination_protocol +g_proxy_address_get_password +g_proxy_address_get_protocol +g_proxy_address_get_uri +g_proxy_address_get_username +g_proxy_address_new +GProxyAddress +g_proxy_connect +g_proxy_connect_async +g_proxy_connect_finish +G_PROXY_EXTENSION_POINT_NAME +g_proxy_get_default_for_protocol +GProxyInterface +G_PROXY_RESOLVER_EXTENSION_POINT_NAME +g_proxy_resolver_get_default +GProxyResolverInterface +g_proxy_resolver_is_supported +g_proxy_resolver_lookup +g_proxy_resolver_lookup_async +g_proxy_resolver_lookup_finish +GProxyResolver +GProxy +g_proxy_supports_hostname +GPtrArray +g_ptr_array_add +g_ptr_array_foreach +g_ptr_array_free +g_ptr_array_index +g_ptr_array_new +g_ptr_array_new_full +g_ptr_array_new_with_free_func +g_ptr_array_ref +g_ptr_array_remove +g_ptr_array_remove_fast +g_ptr_array_remove_index +g_ptr_array_remove_index_fast +g_ptr_array_remove_range +g_ptr_array_set_free_func +g_ptr_array_set_size +g_ptr_array_sized_new +g_ptr_array_sort +g_ptr_array_sort_with_data +g_ptr_array_unref +g_qsort_with_data +GQuark +g_quark_from_static_string +g_quark_from_string +g_quark_to_string +g_quark_try_string +GQueue +g_queue_clear +g_queue_copy +g_queue_delete_link +g_queue_find +g_queue_find_custom +g_queue_foreach +g_queue_free +g_queue_free_full +g_queue_get_length +g_queue_index +g_queue_init +G_QUEUE_INIT +g_queue_insert_after +g_queue_insert_before +g_queue_insert_sorted +g_queue_is_empty +g_queue_link_index +g_queue_new +g_queue_peek_head +g_queue_peek_head_link +g_queue_peek_nth +g_queue_peek_nth_link +g_queue_peek_tail +g_queue_peek_tail_link +g_queue_pop_head +g_queue_pop_head_link +g_queue_pop_nth +g_queue_pop_nth_link +g_queue_pop_tail +g_queue_pop_tail_link +g_queue_push_head +g_queue_push_head_link +g_queue_push_nth +g_queue_push_nth_link +g_queue_push_tail +g_queue_push_tail_link +g_queue_remove +g_queue_remove_all +g_queue_reverse +g_queue_sort +g_queue_unlink +grab_font_from_bitmap +GRand +g_rand_boolean +g_rand_copy +g_rand_double +g_rand_double_range +g_rand_free +g_rand_int +g_rand_int_range +g_rand_new +g_rand_new_with_seed +g_rand_new_with_seed_array +g_random_boolean +g_random_double +g_random_double_range +g_random_int +g_random_int_range +g_random_set_seed +g_rand_set_seed +g_rand_set_seed_array +g_realloc +GReallocFunc +g_realloc_n +GRecMutex +g_rec_mutex_clear +g_rec_mutex_init +g_rec_mutex_lock +g_rec_mutex_trylock +g_rec_mutex_unlock +GRegex +g_regex_check_replacement +G_REGEX_ERROR +g_regex_escape_nul +g_regex_escape_string +GRegexEvalCallback +g_regex_get_capture_count +g_regex_get_compile_flags +g_regex_get_has_cr_or_lf +g_regex_get_match_flags +g_regex_get_max_backref +g_regex_get_pattern +g_regex_get_string_number +g_regex_match +g_regex_match_all +g_regex_match_all_full +g_regex_match_full +g_regex_match_simple +g_regex_new +g_regex_ref +g_regex_replace +g_regex_replace_eval +g_regex_replace_literal +g_regex_split +g_regex_split_full +g_regex_split_simple +g_regex_unref +g_registered_type_info_get_g_type +g_registered_type_info_get_type_init +g_registered_type_info_get_type_name +GRelation +g_relation_count +g_relation_delete +g_relation_destroy +g_relation_exists +g_relation_index +g_relation_insert +g_relation_new +g_relation_print +g_relation_select +g_reload_user_special_dirs_cache +g_remote_action_group_activate_action_full +g_remote_action_group_change_action_state_full +GRemoteActionGroupInterface +GRemoteActionGroup +g_remove +g_rename +g_renew +G_RESOLVER_ERROR +g_resolver_free_addresses +g_resolver_free_targets +g_resolver_get_default +g_resolver_lookup_by_address +g_resolver_lookup_by_address_async +g_resolver_lookup_by_address_finish +g_resolver_lookup_by_name +g_resolver_lookup_by_name_async +g_resolver_lookup_by_name_finish +g_resolver_lookup_records +g_resolver_lookup_records_async +g_resolver_lookup_records_finish +g_resolver_lookup_service +g_resolver_lookup_service_async +g_resolver_lookup_service_finish +g_resolver_set_default +GResolver +GResource +g_resource_enumerate_children +G_RESOURCE_ERROR +g_resource_get_info +g_resource_load +g_resource_lookup_data +g_resource_new_from_data +g_resource_open_stream +g_resource_ref +g_resources_enumerate_children +g_resources_get_info +g_resources_lookup_data +g_resources_open_stream +g_resources_register +g_resources_unregister +g_resource_unref +g_return_if_fail +g_return_if_reached +g_return_val_if_fail +g_return_val_if_reached +g_rmdir +ground_panel +GRWLock +g_rw_lock_clear +g_rw_lock_init +g_rw_lock_reader_lock +g_rw_lock_reader_trylock +g_rw_lock_reader_unlock +g_rw_lock_writer_lock +g_rw_lock_writer_trylock +g_rw_lock_writer_unlock +GScanner +g_scanner_add_symbol +GScannerConfig +g_scanner_cur_line +g_scanner_cur_position +g_scanner_cur_token +g_scanner_cur_value +g_scanner_destroy +g_scanner_eof +g_scanner_error +g_scanner_foreach_symbol +g_scanner_freeze_symbol_table +g_scanner_get_next_token +g_scanner_input_file +g_scanner_input_text +g_scanner_lookup_symbol +GScannerMsgFunc +g_scanner_new +g_scanner_peek_next_token +g_scanner_remove_symbol +g_scanner_scope_add_symbol +g_scanner_scope_foreach_symbol +g_scanner_scope_lookup_symbol +g_scanner_scope_remove_symbol +g_scanner_set_scope +g_scanner_sync_file_offset +g_scanner_thaw_symbol_table +g_scanner_unexp_token +g_scanner_warn +G_SEARCHPATH_SEPARATOR +G_SEARCHPATH_SEPARATOR_S +g_seekable_can_seek +g_seekable_can_truncate +GSeekableIface +g_seekable_seek +GSeekable +g_seekable_tell +g_seekable_truncate +GSequence +g_sequence_append +g_sequence_foreach +g_sequence_foreach_range +g_sequence_free +g_sequence_get +g_sequence_get_begin_iter +g_sequence_get_end_iter +g_sequence_get_iter_at_pos +g_sequence_get_length +g_sequence_insert_before +g_sequence_insert_sorted +g_sequence_insert_sorted_iter +GSequenceIter +g_sequence_iter_compare +GSequenceIterCompareFunc +g_sequence_iter_get_position +g_sequence_iter_get_sequence +g_sequence_iter_is_begin +g_sequence_iter_is_end +g_sequence_iter_move +g_sequence_iter_next +g_sequence_iter_prev +g_sequence_lookup +g_sequence_lookup_iter +g_sequence_move +g_sequence_move_range +g_sequence_new +g_sequence_prepend +g_sequence_range_get_midpoint +g_sequence_remove +g_sequence_remove_range +g_sequence_search +g_sequence_search_iter +g_sequence_set +g_sequence_sort +g_sequence_sort_changed +g_sequence_sort_changed_iter +g_sequence_sort_iter +g_sequence_swap +g_set_application_name +g_setenv +g_set_error +g_set_error_literal +g_set_prgname +g_set_printerr_handler +g_set_print_handler +g_settings_apply +g_settings_backend_changed +g_settings_backend_changed_tree +GSettingsBackendClass +G_SETTINGS_BACKEND_EXTENSION_POINT_NAME +g_settings_backend_flatten_tree +g_settings_backend_get_default +g_settings_backend_keys_changed +g_settings_backend_path_changed +g_settings_backend_path_writable_changed +GSettingsBackend +g_settings_backend_writable_changed +g_settings_bind +GSettingsBindGetMapping +GSettingsBindSetMapping +g_settings_bind_with_mapping +g_settings_bind_writable +g_settings_create_action +g_settings_delay +g_settings_get +g_settings_get_boolean +g_settings_get_child +g_settings_get_double +g_settings_get_enum +g_settings_get_flags +g_settings_get_has_unapplied +g_settings_get_int +g_settings_get_mapped +GSettingsGetMapping +g_settings_get_range +g_settings_get_string +g_settings_get_strv +g_settings_get_uint +g_settings_get_value +g_settings_is_writable +g_settings_list_children +g_settings_list_keys +g_settings_list_relocatable_schemas +g_settings_list_schemas +g_settings_new +g_settings_new_full +g_settings_new_with_backend +g_settings_new_with_backend_and_path +g_settings_new_with_path +g_settings_range_check +g_settings_reset +g_settings_revert +g_settings_schema_get_id +g_settings_schema_get_path +g_settings_schema_ref +GSettingsSchemaSource +g_settings_schema_source_get_default +g_settings_schema_source_lookup +g_settings_schema_source_new_from_directory +g_settings_schema_source_ref +g_settings_schema_source_unref +GSettingsSchema +g_settings_schema_unref +g_settings_set +g_settings_set_boolean +g_settings_set_double +g_settings_set_enum +g_settings_set_flags +g_settings_set_int +g_settings_set_string +g_settings_set_strv +g_settings_set_uint +g_settings_set_value +GSettings +g_settings_sync +g_settings_unbind +G_SHELL_ERROR +g_shell_parse_argv +g_shell_quote +g_shell_unquote +GSignalAccumulator +g_signal_accumulator_first_wins +g_signal_accumulator_true_handled +g_signal_add_emission_hook +g_signal_chain_from_overridden +g_signal_chain_from_overridden_handler +GSignalCMarshaller +g_signal_connect +g_signal_connect_after +g_signal_connect_closure +g_signal_connect_closure_by_id +g_signal_connect_data +g_signal_connect_object +g_signal_connect_swapped +GSignalCVaMarshaller +GSignalEmissionHook +g_signal_emit +g_signal_emit_by_name +g_signal_emitv +g_signal_emit_valist +G_SIGNAL_FLAGS_MASK +g_signal_get_invocation_hint +g_signal_handler_block +g_signal_handler_disconnect +g_signal_handler_find +g_signal_handler_is_connected +g_signal_handlers_block_by_func +g_signal_handlers_block_matched +g_signal_handlers_disconnect_by_data +g_signal_handlers_disconnect_by_func +g_signal_handlers_disconnect_matched +g_signal_handlers_unblock_by_func +g_signal_handlers_unblock_matched +g_signal_handler_unblock +g_signal_has_handler_pending +g_signal_info_get_class_closure +g_signal_info_get_flags +g_signal_info_true_stops_emit +GSignalInvocationHint +g_signal_list_ids +g_signal_lookup +G_SIGNAL_MATCH_MASK +g_signal_name +g_signal_new +g_signal_new_class_handler +g_signal_newv +g_signal_new_valist +g_signal_override_class_closure +g_signal_override_class_handler +g_signal_parse_name +g_signal_query +GSignalQuery +g_signal_remove_emission_hook +g_signal_set_va_marshaller +g_signal_stop_emission +g_signal_stop_emission_by_name +g_signal_type_cclosure_new +G_SIGNAL_TYPE_STATIC_SCOPE +g_simple_action_group_add_entries +g_simple_action_group_insert +g_simple_action_group_lookup +g_simple_action_group_new +g_simple_action_group_remove +GSimpleActionGroup +g_simple_action_new +g_simple_action_new_stateful +g_simple_action_set_enabled +g_simple_action_set_state +GSimpleAction +g_simple_async_report_error_in_idle +g_simple_async_report_gerror_in_idle +g_simple_async_report_take_gerror_in_idle +g_simple_async_result_complete +g_simple_async_result_complete_in_idle +g_simple_async_result_get_op_res_gboolean +g_simple_async_result_get_op_res_gpointer +g_simple_async_result_get_op_res_gssize +g_simple_async_result_get_source_tag +g_simple_async_result_is_valid +g_simple_async_result_new +g_simple_async_result_new_error +g_simple_async_result_new_from_error +g_simple_async_result_new_take_error +g_simple_async_result_propagate_error +g_simple_async_result_run_in_thread +g_simple_async_result_set_check_cancellable +g_simple_async_result_set_error +g_simple_async_result_set_error_va +g_simple_async_result_set_from_error +g_simple_async_result_set_handle_cancellation +g_simple_async_result_set_op_res_gboolean +g_simple_async_result_set_op_res_gpointer +g_simple_async_result_set_op_res_gssize +GSimpleAsyncResult +g_simple_async_result_take_error +GSimpleAsyncThreadFunc +g_simple_permission_new +GSimplePermission +GSIZE_FROM_BE +GSIZE_FROM_LE +GSIZE_TO_BE +GSIZE_TO_LE +GSIZE_TO_POINTER +g_slice_alloc +g_slice_alloc0 +g_slice_copy +g_slice_dup +g_slice_free +g_slice_free1 +g_slice_free_chain +g_slice_free_chain_with_offset +g_slice_new +g_slice_new0 +GSList +g_slist_alloc +g_slist_append +g_slist_concat +g_slist_copy +g_slist_copy_deep +g_slist_delete_link +g_slist_find +g_slist_find_custom +g_slist_foreach +g_slist_free +g_slist_free1 +g_slist_free_1 +g_slist_free_full +g_slist_index +g_slist_insert +g_slist_insert_before +g_slist_insert_sorted +g_slist_insert_sorted_with_data +g_slist_last +g_slist_length +g_slist_next +g_slist_nth +g_slist_nth_data +g_slist_position +g_slist_prepend +g_slist_remove +g_slist_remove_all +g_slist_remove_link +g_slist_reverse +g_slist_sort +g_slist_sort_with_data +g_snprintf +g_socket_accept +g_socket_address_enumerator_next +g_socket_address_enumerator_next_async +g_socket_address_enumerator_next_finish +GSocketAddressEnumerator +g_socket_address_get_family +g_socket_address_get_native_size +g_socket_address_new_from_native +GSocketAddress +g_socket_address_to_native +g_socket_bind +g_socket_check_connect_result +g_socket_client_add_application_proxy +g_socket_client_connect +g_socket_client_connect_async +g_socket_client_connect_finish +g_socket_client_connect_to_host +g_socket_client_connect_to_host_async +g_socket_client_connect_to_host_finish +g_socket_client_connect_to_service +g_socket_client_connect_to_service_async +g_socket_client_connect_to_service_finish +g_socket_client_connect_to_uri +g_socket_client_connect_to_uri_async +g_socket_client_connect_to_uri_finish +g_socket_client_get_enable_proxy +g_socket_client_get_family +g_socket_client_get_local_address +g_socket_client_get_protocol +g_socket_client_get_socket_type +g_socket_client_get_timeout +g_socket_client_get_tls +g_socket_client_get_tls_validation_flags +g_socket_client_new +g_socket_client_set_enable_proxy +g_socket_client_set_family +g_socket_client_set_local_address +g_socket_client_set_protocol +g_socket_client_set_socket_type +g_socket_client_set_timeout +g_socket_client_set_tls +g_socket_client_set_tls_validation_flags +GSocketClient +g_socket_close +g_socket_condition_check +g_socket_condition_timed_wait +g_socket_condition_wait +g_socket_connect +g_socket_connectable_enumerate +GSocketConnectableIface +g_socket_connectable_proxy_enumerate +GSocketConnectable +g_socket_connection_connect +g_socket_connection_connect_async +g_socket_connection_connect_finish +g_socket_connection_factory_create_connection +g_socket_connection_factory_lookup_type +g_socket_connection_factory_register_type +g_socket_connection_get_local_address +g_socket_connection_get_remote_address +g_socket_connection_get_socket +g_socket_connection_is_connected +GSocketConnection +g_socket_control_message_deserialize +g_socket_control_message_get_level +g_socket_control_message_get_msg_type +g_socket_control_message_get_size +g_socket_control_message_serialize +GSocketControlMessage +g_socket_create_source +g_socket_get_available_bytes +g_socket_get_blocking +g_socket_get_broadcast +g_socket_get_credentials +g_socket_get_family +g_socket_get_fd +g_socket_get_keepalive +g_socket_get_listen_backlog +g_socket_get_local_address +g_socket_get_multicast_loopback +g_socket_get_multicast_ttl +g_socket_get_option +g_socket_get_protocol +g_socket_get_remote_address +g_socket_get_socket_type +g_socket_get_timeout +g_socket_get_ttl +g_socket_is_closed +g_socket_is_connected +g_socket_join_multicast_group +g_socket_leave_multicast_group +g_socket_listen +g_socket_listener_accept +g_socket_listener_accept_async +g_socket_listener_accept_finish +g_socket_listener_accept_socket +g_socket_listener_accept_socket_async +g_socket_listener_accept_socket_finish +g_socket_listener_add_address +g_socket_listener_add_any_inet_port +g_socket_listener_add_inet_port +g_socket_listener_add_socket +g_socket_listener_close +g_socket_listener_new +g_socket_listener_set_backlog +GSocketListener +g_socket_new +g_socket_new_from_fd +g_socket_receive +g_socket_receive_from +g_socket_receive_message +g_socket_receive_with_blocking +g_socket_send +g_socket_send_message +g_socket_send_to +g_socket_send_with_blocking +g_socket_service_is_active +g_socket_service_new +g_socket_service_start +g_socket_service_stop +GSocketService +g_socket_set_blocking +g_socket_set_broadcast +g_socket_set_keepalive +g_socket_set_listen_backlog +g_socket_set_multicast_loopback +g_socket_set_multicast_ttl +g_socket_set_option +g_socket_set_timeout +g_socket_set_ttl +g_socket_shutdown +GSocketSourceFunc +g_socket_speaks_ipv4 +GSocket +GSource +g_source_add_child_source +g_source_add_poll +g_source_attach +GSourceCallbackFuncs +G_SOURCE_CONTINUE +g_source_destroy +GSourceDummyMarshal +GSourceFunc +GSourceFuncs +g_source_get_can_recurse +g_source_get_context +g_source_get_current_time +g_source_get_id +g_source_get_name +g_source_get_priority +g_source_get_time +g_source_is_destroyed +g_source_new +g_source_ref +g_source_remove +G_SOURCE_REMOVE +g_source_remove_by_funcs_user_data +g_source_remove_by_user_data +g_source_remove_child_source +g_source_remove_poll +g_source_set_callback +g_source_set_callback_indirect +g_source_set_can_recurse +g_source_set_closure +g_source_set_dummy_callback +g_source_set_funcs +g_source_set_name +g_source_set_name_by_id +g_source_set_priority +g_source_unref +g_spaced_primes_closest +g_spawn_async +g_spawn_async_with_pipes +g_spawn_check_exit_status +GSpawnChildSetupFunc +g_spawn_close_pid +g_spawn_command_line_async +g_spawn_command_line_sync +G_SPAWN_ERROR +g_spawn_sync +g_sprintf +G_SQRT2 +g_srv_target_copy +g_srv_target_free +g_srv_target_get_hostname +g_srv_target_get_port +g_srv_target_get_priority +g_srv_target_get_weight +g_srv_target_list_sort +g_srv_target_new +GSrvTarget +GSSIZE_FROM_BE +GSSIZE_FROM_LE +GSSIZE_TO_BE +GSSIZE_TO_LE +g_stat +GStatBuf +G_STATIC_ASSERT +G_STATIC_ASSERT_EXPR +GStaticMutex +g_static_mutex_free +g_static_mutex_get_mutex +g_static_mutex_init +G_STATIC_MUTEX_INIT +g_static_mutex_lock +g_static_mutex_trylock +g_static_mutex_unlock +GStaticPrivate +g_static_private_free +g_static_private_get +g_static_private_init +G_STATIC_PRIVATE_INIT +g_static_private_set +GStaticRecMutex +g_static_rec_mutex_free +g_static_rec_mutex_init +G_STATIC_REC_MUTEX_INIT +g_static_rec_mutex_lock +g_static_rec_mutex_lock_full +g_static_rec_mutex_trylock +g_static_rec_mutex_unlock +g_static_rec_mutex_unlock_full +GStaticRWLock +g_static_rw_lock_free +g_static_rw_lock_init +G_STATIC_RW_LOCK_INIT +g_static_rw_lock_reader_lock +g_static_rw_lock_reader_trylock +g_static_rw_lock_reader_unlock +g_static_rw_lock_writer_lock +g_static_rw_lock_writer_trylock +g_static_rw_lock_writer_unlock +G_STMT_END +G_STMT_START +g_stpcpy +g_strcanon +g_strcasecmp +g_strchomp +g_strchug +g_strcmp0 +g_strcompress +g_strconcat +g_strdelimit +G_STR_DELIMITERS +g_strdown +g_strdup +g_strdup_printf +g_strdupv +g_strdup_value_contents +g_strdup_vprintf +g_str_equal +g_strerror +g_strescape +g_strfreev +G_STRFUNC +g_str_hash +g_str_has_prefix +g_str_has_suffix +GString +g_string_append +g_string_append_c +g_string_append_len +g_string_append_printf +g_string_append_unichar +g_string_append_uri_escaped +g_string_append_vprintf +g_string_ascii_down +g_string_ascii_up +g_string_assign +GStringChunk +g_string_chunk_clear +g_string_chunk_free +g_string_chunk_insert +g_string_chunk_insert_const +g_string_chunk_insert_len +g_string_chunk_new +g_string_down +g_string_equal +g_string_erase +g_string_free +g_string_free_to_bytes +g_string_hash +G_STRINGIFY +g_string_insert +g_string_insert_c +g_string_insert_len +g_string_insert_unichar +g_string_new +g_string_new_len +g_string_overwrite +g_string_overwrite_len +g_string_prepend +g_string_prepend_c +g_string_prepend_len +g_string_prepend_unichar +g_string_printf +g_string_set_size +g_string_sized_new +g_string_sprintf +g_string_sprintfa +g_string_truncate +g_string_up +g_string_vprintf +g_strip_context +g_strjoin +g_strjoinv +g_strlcat +g_strlcpy +G_STRLOC +g_strncasecmp +g_strndup +g_strnfill +g_strreverse +g_strrstr +g_strrstr_len +g_strsignal +g_strsplit +g_strsplit_set +g_strstrip +g_strstr_len +g_strtod +G_STRUCT_MEMBER +G_STRUCT_MEMBER_P +G_STRUCT_OFFSET +g_strup +GStrv +g_strv_length +g_task_attach_source +g_task_get_cancellable +g_task_get_check_cancellable +g_task_get_context +g_task_get_priority +g_task_get_return_on_cancel +g_task_get_source_object +g_task_get_source_tag +g_task_get_task_data +g_task_had_error +g_task_is_valid +g_task_new +g_task_propagate_boolean +g_task_propagate_int +g_task_propagate_pointer +g_task_report_error +g_task_report_new_error +g_task_return_boolean +g_task_return_error +g_task_return_error_if_cancelled +g_task_return_int +g_task_return_new_error +g_task_return_pointer +g_task_run_in_thread +g_task_run_in_thread_sync +g_task_set_check_cancellable +g_task_set_priority +g_task_set_return_on_cancel +g_task_set_source_tag +g_task_set_task_data +GTask +GTaskThreadFunc +g_tcp_connection_get_graceful_disconnect +g_tcp_connection_set_graceful_disconnect +GTcpConnection +g_tcp_wrapper_connection_get_base_io_stream +g_tcp_wrapper_connection_new +GTcpWrapperConnection +g_test_add +g_test_add_data_func +g_test_add_data_func_full +g_test_add_func +g_test_assert_expected_messages +g_test_bug +g_test_bug_base +GTestCase +g_test_create_case +g_test_create_suite +GTestDataFunc +g_test_dbus_add_service_dir +g_test_dbus_down +g_test_dbus_get_bus_address +g_test_dbus_get_flags +g_test_dbus_new +g_test_dbus_stop +GTestDBus +g_test_dbus_unset +g_test_dbus_up +g_test_expect_message +g_test_fail +GTestFixtureFunc +GTestFunc +g_test_get_root +g_test_init +g_test_initialized +GTestLogFatalFunc +g_test_log_set_fatal_handler +g_test_maximized_result +g_test_message +g_test_minimized_result +g_test_perf +g_test_queue_destroy +g_test_queue_free +g_test_queue_unref +g_test_quick +g_test_quiet +g_test_rand_bit +g_test_rand_double +g_test_rand_double_range +g_test_rand_int +g_test_rand_int_range +g_test_run +g_test_run_suite +g_test_slow +GTestSuite +g_test_suite_add +g_test_suite_add_suite +g_test_thorough +g_test_timer_elapsed +g_test_timer_last +g_test_timer_start +g_test_trap_assert_failed +g_test_trap_assert_passed +g_test_trap_assert_stderr +g_test_trap_assert_stderr_unmatched +g_test_trap_assert_stdout +g_test_trap_assert_stdout_unmatched +g_test_trap_fork +g_test_trap_has_passed +g_test_trap_reached_timeout +g_test_undefined +g_test_verbose +g_themed_icon_append_name +g_themed_icon_get_names +g_themed_icon_new +g_themed_icon_new_from_names +g_themed_icon_new_with_default_fallbacks +g_themed_icon_prepend_name +GThemedIcon +GThread +g_thread_create +g_thread_create_full +g_threaded_socket_service_new +GThreadedSocketService +G_THREAD_ERROR +g_thread_exit +g_thread_foreach +GThreadFunc +g_thread_get_initialized +g_thread_init +g_thread_join +g_thread_new +GThreadPool +g_thread_pool_free +g_thread_pool_get_max_idle_time +g_thread_pool_get_max_threads +g_thread_pool_get_max_unused_threads +g_thread_pool_get_num_threads +g_thread_pool_get_num_unused_threads +g_thread_pool_new +g_thread_pool_push +g_thread_pool_set_max_idle_time +g_thread_pool_set_max_threads +g_thread_pool_set_max_unused_threads +g_thread_pool_set_sort_function +g_thread_pool_stop_unused_threads +g_thread_pool_unprocessed +g_thread_ref +g_thread_self +g_thread_set_priority +G_THREADS_IMPL_POSIX +G_THREADS_IMPL_WIN32 +g_thread_supported +g_thread_try_new +g_thread_unref +g_thread_yield +GTime +g_timeout_add +g_timeout_add_full +g_timeout_add_seconds +g_timeout_add_seconds_full +g_timeout_source_new +g_timeout_source_new_seconds +GTimer +g_timer_continue +g_timer_destroy +g_timer_elapsed +g_timer_new +g_timer_reset +g_timer_start +g_timer_stop +GTimeSpan +G_TIME_SPAN_DAY +G_TIME_SPAN_HOUR +G_TIME_SPAN_MILLISECOND +G_TIME_SPAN_MINUTE +G_TIME_SPAN_SECOND +GTimeVal +g_time_val_add +g_time_val_from_iso8601 +g_time_val_to_iso8601 +GTimeZone +g_time_zone_adjust_time +g_time_zone_find_interval +g_time_zone_get_abbreviation +g_time_zone_get_offset +g_time_zone_is_dst +g_time_zone_new +g_time_zone_new_local +g_time_zone_new_utc +g_time_zone_ref +g_time_zone_unref +GtkAboutDialog +gtk_about_dialog_add_credit_section +gtk_about_dialog_get_artists +gtk_about_dialog_get_authors +gtk_about_dialog_get_comments +gtk_about_dialog_get_copyright +gtk_about_dialog_get_documenters +gtk_about_dialog_get_license +gtk_about_dialog_get_license_type +gtk_about_dialog_get_logo +gtk_about_dialog_get_logo_icon_name +gtk_about_dialog_get_program_name +gtk_about_dialog_get_translator_credits +gtk_about_dialog_get_version +gtk_about_dialog_get_website +gtk_about_dialog_get_website_label +gtk_about_dialog_get_wrap_license +gtk_about_dialog_new +gtk_about_dialog_set_artists +gtk_about_dialog_set_authors +gtk_about_dialog_set_comments +gtk_about_dialog_set_copyright +gtk_about_dialog_set_documenters +gtk_about_dialog_set_license +gtk_about_dialog_set_license_type +gtk_about_dialog_set_logo +gtk_about_dialog_set_logo_icon_name +gtk_about_dialog_set_program_name +gtk_about_dialog_set_translator_credits +gtk_about_dialog_set_version +gtk_about_dialog_set_website +gtk_about_dialog_set_website_label +gtk_about_dialog_set_wrap_license +gtk_accelerator_get_default_mod_mask +gtk_accelerator_get_label +gtk_accelerator_get_label_with_keycode +gtk_accelerator_name +gtk_accelerator_name_with_keycode +gtk_accelerator_parse +gtk_accelerator_parse_with_keycode +gtk_accelerator_set_default_mod_mask +gtk_accelerator_valid +GtkAccelGroup +gtk_accel_group_activate +GtkAccelGroupActivate +gtk_accel_group_connect +gtk_accel_group_connect_by_path +gtk_accel_group_disconnect +gtk_accel_group_disconnect_key +gtk_accel_group_find +GtkAccelGroupFindFunc +gtk_accel_group_from_accel_closure +gtk_accel_group_get_is_locked +gtk_accel_group_get_modifier_mask +gtk_accel_group_lock +gtk_accel_group_new +gtk_accel_groups_activate +gtk_accel_groups_from_object +gtk_accel_group_unlock +GtkAccelKey +GtkAccelLabel +gtk_accel_label_get_accel_widget +gtk_accel_label_get_accel_width +gtk_accel_label_new +gtk_accel_label_refetch +gtk_accel_label_set_accel +gtk_accel_label_set_accel_closure +gtk_accel_label_set_accel_widget +GtkAccelMap +gtk_accel_map_add_entry +gtk_accel_map_add_filter +gtk_accel_map_change_entry +gtk_accel_map_foreach +GtkAccelMapForeach +gtk_accel_map_foreach_unfiltered +gtk_accel_map_get +gtk_accel_map_load +gtk_accel_map_load_fd +gtk_accel_map_load_scanner +gtk_accel_map_lock_path +gtk_accel_map_lookup_entry +gtk_accel_map_save +gtk_accel_map_save_fd +gtk_accel_map_unlock_path +GtkAccessible +gtk_accessible_connect_widget_destroyed +gtk_accessible_get_widget +gtk_accessible_set_widget +GtkAction +GtkActionable +gtk_actionable_get_action_name +gtk_actionable_get_action_target_value +GtkActionableInterface +gtk_actionable_set_action_name +gtk_actionable_set_action_target +gtk_actionable_set_action_target_value +gtk_actionable_set_detailed_action_name +gtk_action_activate +gtk_action_block_activate +gtk_action_connect_accelerator +gtk_action_create_icon +gtk_action_create_menu +gtk_action_create_menu_item +gtk_action_create_tool_item +gtk_action_disconnect_accelerator +GtkActionEntry +gtk_action_get_accel_closure +gtk_action_get_accel_path +gtk_action_get_always_show_image +gtk_action_get_gicon +gtk_action_get_icon_name +gtk_action_get_is_important +gtk_action_get_label +gtk_action_get_name +gtk_action_get_proxies +gtk_action_get_sensitive +gtk_action_get_short_label +gtk_action_get_stock_id +gtk_action_get_tooltip +gtk_action_get_visible +gtk_action_get_visible_horizontal +gtk_action_get_visible_vertical +GtkActionGroup +gtk_action_group_add_action +gtk_action_group_add_actions +gtk_action_group_add_actions_full +gtk_action_group_add_action_with_accel +gtk_action_group_add_radio_actions +gtk_action_group_add_radio_actions_full +gtk_action_group_add_toggle_actions +gtk_action_group_add_toggle_actions_full +gtk_action_group_get_accel_group +gtk_action_group_get_action +gtk_action_group_get_name +gtk_action_group_get_sensitive +gtk_action_group_get_visible +gtk_action_group_list_actions +gtk_action_group_new +gtk_action_group_remove_action +gtk_action_group_set_accel_group +gtk_action_group_set_sensitive +gtk_action_group_set_translate_func +gtk_action_group_set_translation_domain +gtk_action_group_set_visible +gtk_action_group_translate_string +gtk_action_is_sensitive +gtk_action_is_visible +gtk_action_new +gtk_action_set_accel_group +gtk_action_set_accel_path +gtk_action_set_always_show_image +gtk_action_set_gicon +gtk_action_set_icon_name +gtk_action_set_is_important +gtk_action_set_label +gtk_action_set_sensitive +gtk_action_set_short_label +gtk_action_set_stock_id +gtk_action_set_tooltip +gtk_action_set_visible +gtk_action_set_visible_horizontal +gtk_action_set_visible_vertical +gtk_action_unblock_activate +GtkActivatable +gtk_activatable_do_set_related_action +gtk_activatable_get_related_action +gtk_activatable_get_use_action_appearance +GtkActivatableIface +gtk_activatable_set_related_action +gtk_activatable_set_use_action_appearance +gtk_activatable_sync_action_properties +GtkAdjustment +gtk_adjustment_changed +gtk_adjustment_clamp_page +gtk_adjustment_configure +gtk_adjustment_get_lower +gtk_adjustment_get_minimum_increment +gtk_adjustment_get_page_increment +gtk_adjustment_get_page_size +gtk_adjustment_get_step_increment +gtk_adjustment_get_upper +gtk_adjustment_get_value +gtk_adjustment_new +gtk_adjustment_set_lower +gtk_adjustment_set_page_increment +gtk_adjustment_set_page_size +gtk_adjustment_set_step_increment +gtk_adjustment_set_upper +gtk_adjustment_set_value +gtk_adjustment_value_changed +GtkAlignment +gtk_alignment_get_padding +gtk_alignment_new +gtk_alignment_set +gtk_alignment_set_padding +GtkAllocation +gtk_alternative_dialog_button_order +GtkAppChooser +GtkAppChooserButton +gtk_app_chooser_button_append_custom_item +gtk_app_chooser_button_append_separator +gtk_app_chooser_button_get_heading +gtk_app_chooser_button_get_show_default_item +gtk_app_chooser_button_get_show_dialog_item +gtk_app_chooser_button_new +gtk_app_chooser_button_set_active_custom_item +gtk_app_chooser_button_set_heading +gtk_app_chooser_button_set_show_default_item +gtk_app_chooser_button_set_show_dialog_item +GtkAppChooserDialog +gtk_app_chooser_dialog_get_heading +gtk_app_chooser_dialog_get_widget +gtk_app_chooser_dialog_new +gtk_app_chooser_dialog_new_for_content_type +gtk_app_chooser_dialog_set_heading +gtk_app_chooser_get_app_info +gtk_app_chooser_get_content_type +gtk_app_chooser_refresh +GtkAppChooserWidget +gtk_app_chooser_widget_get_default_text +gtk_app_chooser_widget_get_show_all +gtk_app_chooser_widget_get_show_default +gtk_app_chooser_widget_get_show_fallback +gtk_app_chooser_widget_get_show_other +gtk_app_chooser_widget_get_show_recommended +gtk_app_chooser_widget_new +gtk_app_chooser_widget_set_default_text +gtk_app_chooser_widget_set_show_all +gtk_app_chooser_widget_set_show_default +gtk_app_chooser_widget_set_show_fallback +gtk_app_chooser_widget_set_show_other +gtk_app_chooser_widget_set_show_recommended +GtkApplication +gtk_application_add_accelerator +gtk_application_add_window +gtk_application_get_active_window +gtk_application_get_app_menu +gtk_application_get_menubar +gtk_application_get_window_by_id +gtk_application_get_windows +gtk_application_inhibit +gtk_application_is_inhibited +gtk_application_new +gtk_application_remove_accelerator +gtk_application_remove_window +gtk_application_set_app_menu +gtk_application_set_menubar +gtk_application_uninhibit +GtkApplicationWindow +gtk_application_window_get_id +gtk_application_window_get_show_menubar +gtk_application_window_new +gtk_application_window_set_show_menubar +GtkArrow +gtk_arrow_new +gtk_arrow_set +GtkAspectFrame +gtk_aspect_frame_new +gtk_aspect_frame_set +GtkAssistant +gtk_assistant_add_action_widget +gtk_assistant_append_page +gtk_assistant_commit +gtk_assistant_get_current_page +gtk_assistant_get_n_pages +gtk_assistant_get_nth_page +gtk_assistant_get_page_complete +gtk_assistant_get_page_header_image +gtk_assistant_get_page_side_image +gtk_assistant_get_page_title +gtk_assistant_get_page_type +gtk_assistant_insert_page +gtk_assistant_new +gtk_assistant_next_page +GtkAssistantPageFunc +gtk_assistant_prepend_page +gtk_assistant_previous_page +gtk_assistant_remove_action_widget +gtk_assistant_remove_page +gtk_assistant_set_current_page +gtk_assistant_set_forward_page_func +gtk_assistant_set_page_complete +gtk_assistant_set_page_header_image +gtk_assistant_set_page_side_image +gtk_assistant_set_page_title +gtk_assistant_set_page_type +gtk_assistant_update_buttons_state +GtkBin +GTK_BINARY_AGE +GtkBindingArg +GtkBindingEntry +gtk_binding_entry_add_signal +gtk_binding_entry_add_signal_from_string +gtk_binding_entry_add_signall +gtk_binding_entry_remove +gtk_binding_entry_skip +gtk_bindings_activate +gtk_bindings_activate_event +GtkBindingSet +gtk_binding_set_activate +gtk_binding_set_add_path +gtk_binding_set_by_class +gtk_binding_set_find +gtk_binding_set_new +GtkBindingSignal +gtk_bin_get_child +GtkBorder +gtk_border_copy +gtk_border_free +gtk_border_new +GtkBox +gtk_box_get_homogeneous +gtk_box_get_spacing +gtk_box_new +gtk_box_pack_end +gtk_box_pack_start +gtk_box_query_child_packing +gtk_box_reorder_child +gtk_box_set_child_packing +gtk_box_set_homogeneous +gtk_box_set_spacing +GtkBuildable +gtk_buildable_add_child +gtk_buildable_construct_child +gtk_buildable_custom_finished +gtk_buildable_custom_tag_end +gtk_buildable_custom_tag_start +gtk_buildable_get_internal_child +gtk_buildable_get_name +GtkBuildableIface +gtk_buildable_parser_finished +gtk_buildable_set_buildable_property +gtk_buildable_set_name +GtkBuilder +gtk_builder_add_from_file +gtk_builder_add_from_resource +gtk_builder_add_from_string +gtk_builder_add_objects_from_file +gtk_builder_add_objects_from_resource +gtk_builder_add_objects_from_string +GtkBuilderConnectFunc +gtk_builder_connect_signals +gtk_builder_connect_signals_full +GTK_BUILDER_ERROR +gtk_builder_expose_object +gtk_builder_get_object +gtk_builder_get_objects +gtk_builder_get_translation_domain +gtk_builder_get_type_from_name +gtk_builder_new +gtk_builder_set_translation_domain +gtk_builder_value_from_string +gtk_builder_value_from_string_type +GTK_BUILDER_WARN_INVALID_CHILD_TYPE +GtkButton +GtkButtonBox +gtk_button_box_get_child_non_homogeneous +gtk_button_box_get_child_secondary +gtk_button_box_get_layout +gtk_button_box_new +gtk_button_box_set_child_non_homogeneous +gtk_button_box_set_child_secondary +gtk_button_box_set_layout +gtk_button_clicked +gtk_button_enter +gtk_button_get_alignment +gtk_button_get_always_show_image +gtk_button_get_event_window +gtk_button_get_focus_on_click +gtk_button_get_image +gtk_button_get_image_position +gtk_button_get_label +gtk_button_get_relief +gtk_button_get_use_stock +gtk_button_get_use_underline +gtk_button_leave +gtk_button_new +gtk_button_new_from_stock +gtk_button_new_with_label +gtk_button_new_with_mnemonic +gtk_button_pressed +gtk_button_released +gtk_button_set_alignment +gtk_button_set_always_show_image +gtk_button_set_focus_on_click +gtk_button_set_image +gtk_button_set_image_position +gtk_button_set_label +gtk_button_set_relief +gtk_button_set_use_stock +gtk_button_set_use_underline +gtk_cairo_should_draw_window +gtk_cairo_transform_to_window +GtkCalendar +gtk_calendar_clear_marks +GtkCalendarDetailFunc +gtk_calendar_get_date +gtk_calendar_get_day_is_marked +gtk_calendar_get_detail_height_rows +gtk_calendar_get_detail_width_chars +gtk_calendar_get_display_options +gtk_calendar_mark_day +gtk_calendar_new +gtk_calendar_select_day +gtk_calendar_select_month +gtk_calendar_set_detail_func +gtk_calendar_set_detail_height_rows +gtk_calendar_set_detail_width_chars +gtk_calendar_set_display_options +gtk_calendar_unmark_day +GtkCallback +GtkCellAllocCallback +GtkCellArea +gtk_cell_area_activate +gtk_cell_area_activate_cell +gtk_cell_area_add +gtk_cell_area_add_focus_sibling +gtk_cell_area_add_with_properties +gtk_cell_area_apply_attributes +gtk_cell_area_attribute_connect +gtk_cell_area_attribute_disconnect +GtkCellAreaBox +GtkCellAreaBoxClass +gtk_cell_area_box_get_spacing +gtk_cell_area_box_new +gtk_cell_area_box_pack_end +gtk_cell_area_box_pack_start +gtk_cell_area_box_set_spacing +gtk_cell_area_cell_get +gtk_cell_area_cell_get_property +gtk_cell_area_cell_get_valist +gtk_cell_area_cell_set +gtk_cell_area_cell_set_property +gtk_cell_area_cell_set_valist +GtkCellAreaClass +gtk_cell_area_class_find_cell_property +gtk_cell_area_class_install_cell_property +gtk_cell_area_class_list_cell_properties +GtkCellAreaContext +gtk_cell_area_context_allocate +GtkCellAreaContextClass +gtk_cell_area_context_get_allocation +gtk_cell_area_context_get_area +gtk_cell_area_context_get_preferred_height +gtk_cell_area_context_get_preferred_height_for_width +gtk_cell_area_context_get_preferred_width +gtk_cell_area_context_get_preferred_width_for_height +gtk_cell_area_context_push_preferred_height +gtk_cell_area_context_push_preferred_width +gtk_cell_area_context_reset +gtk_cell_area_copy_context +gtk_cell_area_create_context +gtk_cell_area_event +gtk_cell_area_focus +gtk_cell_area_foreach +gtk_cell_area_foreach_alloc +gtk_cell_area_get_cell_allocation +gtk_cell_area_get_cell_at_position +gtk_cell_area_get_current_path_string +gtk_cell_area_get_edited_cell +gtk_cell_area_get_edit_widget +gtk_cell_area_get_focus_cell +gtk_cell_area_get_focus_from_sibling +gtk_cell_area_get_focus_siblings +gtk_cell_area_get_preferred_height +gtk_cell_area_get_preferred_height_for_width +gtk_cell_area_get_preferred_width +gtk_cell_area_get_preferred_width_for_height +gtk_cell_area_get_request_mode +gtk_cell_area_has_renderer +gtk_cell_area_inner_cell_area +gtk_cell_area_is_activatable +gtk_cell_area_is_focus_sibling +gtk_cell_area_remove +gtk_cell_area_remove_focus_sibling +gtk_cell_area_render +gtk_cell_area_request_renderer +gtk_cell_area_set_focus_cell +gtk_cell_area_stop_editing +GTK_CELL_AREA_WARN_INVALID_CELL_PROPERTY_ID +GtkCellCallback +GtkCellEditable +gtk_cell_editable_editing_done +GtkCellEditableIface +gtk_cell_editable_remove_widget +gtk_cell_editable_start_editing +GtkCellLayout +gtk_cell_layout_add_attribute +gtk_cell_layout_clear +gtk_cell_layout_clear_attributes +GtkCellLayoutDataFunc +gtk_cell_layout_get_area +gtk_cell_layout_get_cells +GtkCellLayoutIface +gtk_cell_layout_pack_end +gtk_cell_layout_pack_start +gtk_cell_layout_reorder +gtk_cell_layout_set_attributes +gtk_cell_layout_set_cell_data_func +GtkCellRenderer +GtkCellRendererAccel +gtk_cell_renderer_accel_new +gtk_cell_renderer_activate +GtkCellRendererClass +GtkCellRendererCombo +gtk_cell_renderer_combo_new +gtk_cell_renderer_get_aligned_area +gtk_cell_renderer_get_alignment +gtk_cell_renderer_get_fixed_size +gtk_cell_renderer_get_padding +gtk_cell_renderer_get_preferred_height +gtk_cell_renderer_get_preferred_height_for_width +gtk_cell_renderer_get_preferred_size +gtk_cell_renderer_get_preferred_width +gtk_cell_renderer_get_preferred_width_for_height +gtk_cell_renderer_get_request_mode +gtk_cell_renderer_get_sensitive +gtk_cell_renderer_get_size +gtk_cell_renderer_get_state +gtk_cell_renderer_get_visible +gtk_cell_renderer_is_activatable +GtkCellRendererPixbuf +gtk_cell_renderer_pixbuf_new +GtkCellRendererProgress +gtk_cell_renderer_progress_new +gtk_cell_renderer_render +gtk_cell_renderer_set_alignment +gtk_cell_renderer_set_fixed_size +gtk_cell_renderer_set_padding +gtk_cell_renderer_set_sensitive +gtk_cell_renderer_set_visible +GtkCellRendererSpin +GtkCellRendererSpinner +gtk_cell_renderer_spinner_new +gtk_cell_renderer_spin_new +gtk_cell_renderer_start_editing +gtk_cell_renderer_stop_editing +GtkCellRendererText +gtk_cell_renderer_text_new +gtk_cell_renderer_text_set_fixed_height_from_font +GtkCellRendererToggle +gtk_cell_renderer_toggle_get_activatable +gtk_cell_renderer_toggle_get_active +gtk_cell_renderer_toggle_get_radio +gtk_cell_renderer_toggle_new +gtk_cell_renderer_toggle_set_activatable +gtk_cell_renderer_toggle_set_active +gtk_cell_renderer_toggle_set_radio +GtkCellView +gtk_cell_view_get_displayed_row +gtk_cell_view_get_draw_sensitive +gtk_cell_view_get_fit_model +gtk_cell_view_get_model +gtk_cell_view_get_size_of_row +gtk_cell_view_new +gtk_cell_view_new_with_context +gtk_cell_view_new_with_markup +gtk_cell_view_new_with_pixbuf +gtk_cell_view_new_with_text +gtk_cell_view_set_background_color +gtk_cell_view_set_background_rgba +gtk_cell_view_set_displayed_row +gtk_cell_view_set_draw_sensitive +gtk_cell_view_set_fit_model +gtk_cell_view_set_model +GtkCheckButton +gtk_check_button_new +gtk_check_button_new_with_label +gtk_check_button_new_with_mnemonic +GtkCheckMenuItem +gtk_check_menu_item_get_active +gtk_check_menu_item_get_draw_as_radio +gtk_check_menu_item_get_inconsistent +gtk_check_menu_item_new +gtk_check_menu_item_new_with_label +gtk_check_menu_item_new_with_mnemonic +gtk_check_menu_item_set_active +gtk_check_menu_item_set_draw_as_radio +gtk_check_menu_item_set_inconsistent +gtk_check_menu_item_toggled +gtk_check_version +GTK_CHECK_VERSION +GtkClipboard +gtk_clipboard_clear +GtkClipboardClearFunc +gtk_clipboard_get +gtk_clipboard_get_display +gtk_clipboard_get_for_display +GtkClipboardGetFunc +gtk_clipboard_get_owner +GtkClipboardImageReceivedFunc +GtkClipboardReceivedFunc +gtk_clipboard_request_contents +gtk_clipboard_request_image +gtk_clipboard_request_rich_text +gtk_clipboard_request_targets +gtk_clipboard_request_text +gtk_clipboard_request_uris +GtkClipboardRichTextReceivedFunc +gtk_clipboard_set_can_store +gtk_clipboard_set_image +gtk_clipboard_set_text +gtk_clipboard_set_with_data +gtk_clipboard_set_with_owner +gtk_clipboard_store +GtkClipboardTargetsReceivedFunc +GtkClipboardTextReceivedFunc +GtkClipboardURIReceivedFunc +gtk_clipboard_wait_for_contents +gtk_clipboard_wait_for_image +gtk_clipboard_wait_for_rich_text +gtk_clipboard_wait_for_targets +gtk_clipboard_wait_for_text +gtk_clipboard_wait_for_uris +gtk_clipboard_wait_is_image_available +gtk_clipboard_wait_is_rich_text_available +gtk_clipboard_wait_is_target_available +gtk_clipboard_wait_is_text_available +gtk_clipboard_wait_is_uris_available +GtkColorButton +gtk_color_button_get_alpha +gtk_color_button_get_color +gtk_color_button_get_rgba +gtk_color_button_get_title +gtk_color_button_get_use_alpha +gtk_color_button_new +gtk_color_button_new_with_color +gtk_color_button_new_with_rgba +gtk_color_button_set_alpha +gtk_color_button_set_color +gtk_color_button_set_rgba +gtk_color_button_set_title +gtk_color_button_set_use_alpha +GtkColorChooser +gtk_color_chooser_add_palette +GtkColorChooserDialog +gtk_color_chooser_dialog_new +gtk_color_chooser_get_rgba +gtk_color_chooser_get_use_alpha +gtk_color_chooser_set_rgba +gtk_color_chooser_set_use_alpha +GtkColorChooserWidget +gtk_color_chooser_widget_new +GtkColorSelection +GtkColorSelectionChangePaletteFunc +GtkColorSelectionChangePaletteWithScreenFunc +GtkColorSelectionDialog +gtk_color_selection_dialog_get_color_selection +gtk_color_selection_dialog_new +gtk_color_selection_get_current_alpha +gtk_color_selection_get_current_color +gtk_color_selection_get_current_rgba +gtk_color_selection_get_has_opacity_control +gtk_color_selection_get_has_palette +gtk_color_selection_get_previous_alpha +gtk_color_selection_get_previous_color +gtk_color_selection_get_previous_rgba +gtk_color_selection_is_adjusting +gtk_color_selection_new +gtk_color_selection_palette_from_string +gtk_color_selection_palette_to_string +gtk_color_selection_set_change_palette_with_screen_hook +gtk_color_selection_set_current_alpha +gtk_color_selection_set_current_color +gtk_color_selection_set_current_rgba +gtk_color_selection_set_has_opacity_control +gtk_color_selection_set_has_palette +gtk_color_selection_set_previous_alpha +gtk_color_selection_set_previous_color +gtk_color_selection_set_previous_rgba +GtkComboBox +gtk_combo_box_get_active +gtk_combo_box_get_active_id +gtk_combo_box_get_active_iter +gtk_combo_box_get_add_tearoffs +gtk_combo_box_get_button_sensitivity +gtk_combo_box_get_column_span_column +gtk_combo_box_get_entry_text_column +gtk_combo_box_get_focus_on_click +gtk_combo_box_get_has_entry +gtk_combo_box_get_id_column +gtk_combo_box_get_model +gtk_combo_box_get_popup_accessible +gtk_combo_box_get_popup_fixed_width +gtk_combo_box_get_row_separator_func +gtk_combo_box_get_row_span_column +gtk_combo_box_get_title +gtk_combo_box_get_wrap_width +gtk_combo_box_new +gtk_combo_box_new_with_area +gtk_combo_box_new_with_area_and_entry +gtk_combo_box_new_with_entry +gtk_combo_box_new_with_model +gtk_combo_box_new_with_model_and_entry +gtk_combo_box_popdown +gtk_combo_box_popup +gtk_combo_box_popup_for_device +gtk_combo_box_set_active +gtk_combo_box_set_active_id +gtk_combo_box_set_active_iter +gtk_combo_box_set_add_tearoffs +gtk_combo_box_set_button_sensitivity +gtk_combo_box_set_column_span_column +gtk_combo_box_set_entry_text_column +gtk_combo_box_set_focus_on_click +gtk_combo_box_set_id_column +gtk_combo_box_set_model +gtk_combo_box_set_popup_fixed_width +gtk_combo_box_set_row_separator_func +gtk_combo_box_set_row_span_column +gtk_combo_box_set_title +gtk_combo_box_set_wrap_width +GtkComboBoxText +gtk_combo_box_text_append +gtk_combo_box_text_append_text +gtk_combo_box_text_get_active_text +gtk_combo_box_text_insert +gtk_combo_box_text_insert_text +gtk_combo_box_text_new +gtk_combo_box_text_new_with_entry +gtk_combo_box_text_prepend +gtk_combo_box_text_prepend_text +gtk_combo_box_text_remove +gtk_combo_box_text_remove_all +GtkContainer +gtk_container_add +gtk_container_add_with_properties +gtk_container_check_resize +gtk_container_child_get +gtk_container_child_get_property +gtk_container_child_get_valist +gtk_container_child_notify +gtk_container_child_set +gtk_container_child_set_property +gtk_container_child_set_valist +gtk_container_child_type +gtk_container_class_find_child_property +gtk_container_class_handle_border_width +gtk_container_class_install_child_property +gtk_container_class_list_child_properties +gtk_container_forall +gtk_container_foreach +gtk_container_get_border_width +gtk_container_get_children +gtk_container_get_focus_chain +gtk_container_get_focus_child +gtk_container_get_focus_hadjustment +gtk_container_get_focus_vadjustment +gtk_container_get_path_for_child +gtk_container_get_resize_mode +gtk_container_propagate_draw +gtk_container_remove +gtk_container_resize_children +gtk_container_set_border_width +gtk_container_set_focus_chain +gtk_container_set_focus_child +gtk_container_set_focus_hadjustment +gtk_container_set_focus_vadjustment +gtk_container_set_reallocate_redraws +gtk_container_set_resize_mode +gtk_container_unset_focus_chain +GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID +GtkCssProvider +GTK_CSS_PROVIDER_ERROR +gtk_css_provider_get_default +gtk_css_provider_get_named +gtk_css_provider_load_from_data +gtk_css_provider_load_from_file +gtk_css_provider_load_from_path +gtk_css_provider_new +gtk_css_provider_to_string +GtkCssSection +gtk_css_section_get_end_line +gtk_css_section_get_end_position +gtk_css_section_get_file +gtk_css_section_get_parent +gtk_css_section_get_section_type +gtk_css_section_get_start_line +gtk_css_section_get_start_position +gtk_css_section_ref +gtk_css_section_unref +gtk_device_grab_add +gtk_device_grab_remove +GtkDialog +gtk_dialog_add_action_widget +gtk_dialog_add_button +gtk_dialog_add_buttons +gtk_dialog_get_action_area +gtk_dialog_get_content_area +gtk_dialog_get_response_for_widget +gtk_dialog_get_widget_for_response +gtk_dialog_new +gtk_dialog_new_with_buttons +gtk_dialog_response +gtk_dialog_run +gtk_dialog_set_alternative_button_order +gtk_dialog_set_alternative_button_order_from_array +gtk_dialog_set_default_response +gtk_dialog_set_response_sensitive +gtk_disable_setlocale +gtk_distribute_natural_allocation +gtk_drag_begin +gtk_drag_check_threshold +gtk_drag_dest_add_image_targets +gtk_drag_dest_add_text_targets +gtk_drag_dest_add_uri_targets +gtk_drag_dest_find_target +gtk_drag_dest_get_target_list +gtk_drag_dest_get_track_motion +gtk_drag_dest_set +gtk_drag_dest_set_proxy +gtk_drag_dest_set_target_list +gtk_drag_dest_set_track_motion +gtk_drag_dest_unset +gtk_drag_finish +gtk_drag_get_data +gtk_drag_get_source_widget +gtk_drag_highlight +gtk_drag_set_icon_default +gtk_drag_set_icon_gicon +gtk_drag_set_icon_name +gtk_drag_set_icon_pixbuf +gtk_drag_set_icon_stock +gtk_drag_set_icon_surface +gtk_drag_set_icon_widget +gtk_drag_source_add_image_targets +gtk_drag_source_add_text_targets +gtk_drag_source_add_uri_targets +gtk_drag_source_get_target_list +gtk_drag_source_set +gtk_drag_source_set_icon_gicon +gtk_drag_source_set_icon_name +gtk_drag_source_set_icon_pixbuf +gtk_drag_source_set_icon_stock +gtk_drag_source_set_target_list +gtk_drag_source_unset +gtk_drag_unhighlight +GtkDrawingArea +gtk_drawing_area_new +gtk_draw_insertion_cursor +GtkEditable +gtk_editable_copy_clipboard +gtk_editable_cut_clipboard +gtk_editable_delete_selection +gtk_editable_delete_text +gtk_editable_get_chars +gtk_editable_get_editable +gtk_editable_get_position +gtk_editable_get_selection_bounds +gtk_editable_insert_text +gtk_editable_paste_clipboard +gtk_editable_select_region +gtk_editable_set_editable +gtk_editable_set_position +GtkEntry +GtkEntryBuffer +gtk_entry_buffer_delete_text +gtk_entry_buffer_emit_deleted_text +gtk_entry_buffer_emit_inserted_text +gtk_entry_buffer_get_bytes +gtk_entry_buffer_get_length +gtk_entry_buffer_get_max_length +gtk_entry_buffer_get_text +gtk_entry_buffer_insert_text +gtk_entry_buffer_new +gtk_entry_buffer_set_max_length +gtk_entry_buffer_set_text +GtkEntryCompletion +gtk_entry_completion_complete +gtk_entry_completion_compute_prefix +gtk_entry_completion_delete_action +gtk_entry_completion_get_completion_prefix +gtk_entry_completion_get_entry +gtk_entry_completion_get_inline_completion +gtk_entry_completion_get_inline_selection +gtk_entry_completion_get_minimum_key_length +gtk_entry_completion_get_model +gtk_entry_completion_get_popup_completion +gtk_entry_completion_get_popup_set_width +gtk_entry_completion_get_popup_single_match +gtk_entry_completion_get_text_column +gtk_entry_completion_insert_action_markup +gtk_entry_completion_insert_action_text +gtk_entry_completion_insert_prefix +GtkEntryCompletionMatchFunc +gtk_entry_completion_new +gtk_entry_completion_new_with_area +gtk_entry_completion_set_inline_completion +gtk_entry_completion_set_inline_selection +gtk_entry_completion_set_match_func +gtk_entry_completion_set_minimum_key_length +gtk_entry_completion_set_model +gtk_entry_completion_set_popup_completion +gtk_entry_completion_set_popup_set_width +gtk_entry_completion_set_popup_single_match +gtk_entry_completion_set_text_column +gtk_entry_get_activates_default +gtk_entry_get_alignment +gtk_entry_get_attributes +gtk_entry_get_buffer +gtk_entry_get_completion +gtk_entry_get_current_icon_drag_source +gtk_entry_get_cursor_hadjustment +gtk_entry_get_has_frame +gtk_entry_get_icon_activatable +gtk_entry_get_icon_area +gtk_entry_get_icon_at_pos +gtk_entry_get_icon_gicon +gtk_entry_get_icon_name +gtk_entry_get_icon_pixbuf +gtk_entry_get_icon_sensitive +gtk_entry_get_icon_stock +gtk_entry_get_icon_storage_type +gtk_entry_get_icon_tooltip_markup +gtk_entry_get_icon_tooltip_text +gtk_entry_get_inner_border +gtk_entry_get_input_hints +gtk_entry_get_input_purpose +gtk_entry_get_invisible_char +gtk_entry_get_layout +gtk_entry_get_layout_offsets +gtk_entry_get_max_length +gtk_entry_get_overwrite_mode +gtk_entry_get_placeholder_text +gtk_entry_get_progress_fraction +gtk_entry_get_progress_pulse_step +gtk_entry_get_text +gtk_entry_get_text_area +gtk_entry_get_text_length +gtk_entry_get_visibility +gtk_entry_get_width_chars +gtk_entry_im_context_filter_keypress +gtk_entry_layout_index_to_text_index +gtk_entry_new +gtk_entry_new_with_buffer +gtk_entry_progress_pulse +gtk_entry_reset_im_context +gtk_entry_set_activates_default +gtk_entry_set_alignment +gtk_entry_set_attributes +gtk_entry_set_buffer +gtk_entry_set_completion +gtk_entry_set_cursor_hadjustment +gtk_entry_set_has_frame +gtk_entry_set_icon_activatable +gtk_entry_set_icon_drag_source +gtk_entry_set_icon_from_gicon +gtk_entry_set_icon_from_icon_name +gtk_entry_set_icon_from_pixbuf +gtk_entry_set_icon_from_stock +gtk_entry_set_icon_sensitive +gtk_entry_set_icon_tooltip_markup +gtk_entry_set_icon_tooltip_text +gtk_entry_set_inner_border +gtk_entry_set_input_hints +gtk_entry_set_input_purpose +gtk_entry_set_invisible_char +gtk_entry_set_max_length +gtk_entry_set_overwrite_mode +gtk_entry_set_placeholder_text +gtk_entry_set_progress_fraction +gtk_entry_set_progress_pulse_step +gtk_entry_set_text +gtk_entry_set_visibility +gtk_entry_set_width_chars +gtk_entry_text_index_to_layout_index +gtk_entry_unset_invisible_char +gtk_enumerate_printers +GtkEventBox +gtk_event_box_get_above_child +gtk_event_box_get_visible_window +gtk_event_box_new +gtk_event_box_set_above_child +gtk_event_box_set_visible_window +gtk_events_pending +GtkExpander +gtk_expander_get_expanded +gtk_expander_get_label +gtk_expander_get_label_fill +gtk_expander_get_label_widget +gtk_expander_get_resize_toplevel +gtk_expander_get_spacing +gtk_expander_get_use_markup +gtk_expander_get_use_underline +gtk_expander_new +gtk_expander_new_with_mnemonic +gtk_expander_set_expanded +gtk_expander_set_label +gtk_expander_set_label_fill +gtk_expander_set_label_widget +gtk_expander_set_resize_toplevel +gtk_expander_set_spacing +gtk_expander_set_use_markup +gtk_expander_set_use_underline +gtk_false +GtkFileChooser +gtk_file_chooser_add_filter +gtk_file_chooser_add_shortcut_folder +gtk_file_chooser_add_shortcut_folder_uri +GtkFileChooserButton +gtk_file_chooser_button_get_focus_on_click +gtk_file_chooser_button_get_title +gtk_file_chooser_button_get_width_chars +gtk_file_chooser_button_new +gtk_file_chooser_button_new_with_dialog +gtk_file_chooser_button_set_focus_on_click +gtk_file_chooser_button_set_title +gtk_file_chooser_button_set_width_chars +GtkFileChooserDialog +gtk_file_chooser_dialog_new +GTK_FILE_CHOOSER_ERROR +gtk_file_chooser_get_action +gtk_file_chooser_get_create_folders +gtk_file_chooser_get_current_folder +gtk_file_chooser_get_current_folder_file +gtk_file_chooser_get_current_folder_uri +gtk_file_chooser_get_do_overwrite_confirmation +gtk_file_chooser_get_extra_widget +gtk_file_chooser_get_file +gtk_file_chooser_get_filename +gtk_file_chooser_get_filenames +gtk_file_chooser_get_files +gtk_file_chooser_get_filter +gtk_file_chooser_get_local_only +gtk_file_chooser_get_preview_file +gtk_file_chooser_get_preview_filename +gtk_file_chooser_get_preview_uri +gtk_file_chooser_get_preview_widget +gtk_file_chooser_get_preview_widget_active +gtk_file_chooser_get_select_multiple +gtk_file_chooser_get_show_hidden +gtk_file_chooser_get_uri +gtk_file_chooser_get_uris +gtk_file_chooser_get_use_preview_label +gtk_file_chooser_list_filters +gtk_file_chooser_list_shortcut_folders +gtk_file_chooser_list_shortcut_folder_uris +gtk_file_chooser_remove_filter +gtk_file_chooser_remove_shortcut_folder +gtk_file_chooser_remove_shortcut_folder_uri +gtk_file_chooser_select_all +gtk_file_chooser_select_file +gtk_file_chooser_select_filename +gtk_file_chooser_select_uri +gtk_file_chooser_set_action +gtk_file_chooser_set_create_folders +gtk_file_chooser_set_current_folder +gtk_file_chooser_set_current_folder_file +gtk_file_chooser_set_current_folder_uri +gtk_file_chooser_set_current_name +gtk_file_chooser_set_do_overwrite_confirmation +gtk_file_chooser_set_extra_widget +gtk_file_chooser_set_file +gtk_file_chooser_set_filename +gtk_file_chooser_set_filter +gtk_file_chooser_set_local_only +gtk_file_chooser_set_preview_widget +gtk_file_chooser_set_preview_widget_active +gtk_file_chooser_set_select_multiple +gtk_file_chooser_set_show_hidden +gtk_file_chooser_set_uri +gtk_file_chooser_set_use_preview_label +gtk_file_chooser_unselect_all +gtk_file_chooser_unselect_file +gtk_file_chooser_unselect_filename +gtk_file_chooser_unselect_uri +GtkFileChooserWidget +gtk_file_chooser_widget_new +GtkFileFilter +gtk_file_filter_add_custom +gtk_file_filter_add_mime_type +gtk_file_filter_add_pattern +gtk_file_filter_add_pixbuf_formats +gtk_file_filter_filter +GtkFileFilterFunc +gtk_file_filter_get_name +gtk_file_filter_get_needed +GtkFileFilterInfo +gtk_file_filter_new +gtk_file_filter_set_name +GtkFixed +gtk_fixed_move +gtk_fixed_new +gtk_fixed_put +GtkFontButton +gtk_font_button_get_font_name +gtk_font_button_get_show_size +gtk_font_button_get_show_style +gtk_font_button_get_title +gtk_font_button_get_use_font +gtk_font_button_get_use_size +gtk_font_button_new +gtk_font_button_new_with_font +gtk_font_button_set_font_name +gtk_font_button_set_show_size +gtk_font_button_set_show_style +gtk_font_button_set_title +gtk_font_button_set_use_font +gtk_font_button_set_use_size +GtkFontChooser +GtkFontChooserDialog +gtk_font_chooser_dialog_new +gtk_font_chooser_get_font +gtk_font_chooser_get_font_desc +gtk_font_chooser_get_font_face +gtk_font_chooser_get_font_family +gtk_font_chooser_get_font_size +gtk_font_chooser_get_preview_text +gtk_font_chooser_get_show_preview_entry +gtk_font_chooser_set_filter_func +gtk_font_chooser_set_font +gtk_font_chooser_set_font_desc +gtk_font_chooser_set_preview_text +gtk_font_chooser_set_show_preview_entry +GtkFontChooserWidget +gtk_font_chooser_widget_new +GtkFontFilterFunc +GtkFontSelection +GtkFontSelectionDialog +gtk_font_selection_dialog_get_cancel_button +gtk_font_selection_dialog_get_font_name +gtk_font_selection_dialog_get_font_selection +gtk_font_selection_dialog_get_ok_button +gtk_font_selection_dialog_get_preview_text +gtk_font_selection_dialog_new +gtk_font_selection_dialog_set_font_name +gtk_font_selection_dialog_set_preview_text +gtk_font_selection_get_face +gtk_font_selection_get_face_list +gtk_font_selection_get_family +gtk_font_selection_get_family_list +gtk_font_selection_get_font_name +gtk_font_selection_get_preview_entry +gtk_font_selection_get_preview_text +gtk_font_selection_get_size +gtk_font_selection_get_size_entry +gtk_font_selection_get_size_list +gtk_font_selection_new +gtk_font_selection_set_font_name +gtk_font_selection_set_preview_text +GtkFrame +gtk_frame_get_label +gtk_frame_get_label_align +gtk_frame_get_label_widget +gtk_frame_get_shadow_type +gtk_frame_new +gtk_frame_set_label +gtk_frame_set_label_align +gtk_frame_set_label_widget +gtk_frame_set_shadow_type +gtk_get_binary_age +gtk_get_current_event +gtk_get_current_event_device +gtk_get_current_event_state +gtk_get_current_event_time +gtk_get_default_language +gtk_get_event_widget +gtk_get_interface_age +gtk_get_major_version +gtk_get_micro_version +gtk_get_minor_version +gtk_get_option_group +gtk_grab_add +gtk_grab_get_current +gtk_grab_remove +GtkGradient +gtk_gradient_add_color_stop +gtk_gradient_new_linear +gtk_gradient_new_radial +gtk_gradient_ref +gtk_gradient_resolve +gtk_gradient_resolve_for_context +gtk_gradient_to_string +gtk_gradient_unref +GtkGrid +gtk_grid_attach +gtk_grid_attach_next_to +gtk_grid_get_child_at +gtk_grid_get_column_homogeneous +gtk_grid_get_column_spacing +gtk_grid_get_row_homogeneous +gtk_grid_get_row_spacing +gtk_grid_insert_column +gtk_grid_insert_next_to +gtk_grid_insert_row +gtk_grid_new +gtk_grid_set_column_homogeneous +gtk_grid_set_column_spacing +gtk_grid_set_row_homogeneous +gtk_grid_set_row_spacing +GtkHandleBox +gtk_handle_box_get_child_detached +gtk_handle_box_get_handle_position +gtk_handle_box_get_shadow_type +gtk_handle_box_get_snap_edge +gtk_handle_box_new +gtk_handle_box_set_handle_position +gtk_handle_box_set_shadow_type +gtk_handle_box_set_snap_edge +GtkHBox +gtk_hbox_new +GtkHButtonBox +gtk_hbutton_box_new +GtkHPaned +gtk_hpaned_new +GtkHScale +gtk_hscale_new +gtk_hscale_new_with_range +GtkHScrollbar +gtk_hscrollbar_new +GtkHSeparator +gtk_hseparator_new +GtkHSV +gtk_hsv_get_color +gtk_hsv_get_metrics +gtk_hsv_is_adjusting +gtk_hsv_new +gtk_hsv_set_color +gtk_hsv_set_metrics +gtk_hsv_to_rgb +GtkIconFactory +gtk_icon_factory_add +gtk_icon_factory_add_default +gtk_icon_factory_lookup +gtk_icon_factory_lookup_default +gtk_icon_factory_new +gtk_icon_factory_remove_default +GtkIconInfo +gtk_icon_info_copy +gtk_icon_info_free +gtk_icon_info_get_attach_points +gtk_icon_info_get_base_size +gtk_icon_info_get_builtin_pixbuf +gtk_icon_info_get_display_name +gtk_icon_info_get_embedded_rect +gtk_icon_info_get_filename +gtk_icon_info_load_icon +gtk_icon_info_load_symbolic +gtk_icon_info_load_symbolic_for_context +gtk_icon_info_load_symbolic_for_style +gtk_icon_info_new_for_pixbuf +gtk_icon_info_set_raw_coordinates +GtkIconSet +gtk_icon_set_add_source +gtk_icon_set_copy +gtk_icon_set_get_sizes +gtk_icon_set_new +gtk_icon_set_new_from_pixbuf +gtk_icon_set_ref +gtk_icon_set_render_icon +gtk_icon_set_render_icon_pixbuf +gtk_icon_set_unref +gtk_icon_size_from_name +gtk_icon_size_get_name +gtk_icon_size_lookup +gtk_icon_size_lookup_for_settings +gtk_icon_size_register +gtk_icon_size_register_alias +GtkIconSource +gtk_icon_source_copy +gtk_icon_source_free +gtk_icon_source_get_direction +gtk_icon_source_get_direction_wildcarded +gtk_icon_source_get_filename +gtk_icon_source_get_icon_name +gtk_icon_source_get_pixbuf +gtk_icon_source_get_size +gtk_icon_source_get_size_wildcarded +gtk_icon_source_get_state +gtk_icon_source_get_state_wildcarded +gtk_icon_source_new +gtk_icon_source_set_direction +gtk_icon_source_set_direction_wildcarded +gtk_icon_source_set_filename +gtk_icon_source_set_icon_name +gtk_icon_source_set_pixbuf +gtk_icon_source_set_size +gtk_icon_source_set_size_wildcarded +gtk_icon_source_set_state +gtk_icon_source_set_state_wildcarded +GtkIconTheme +gtk_icon_theme_add_builtin_icon +gtk_icon_theme_append_search_path +gtk_icon_theme_choose_icon +GTK_ICON_THEME_ERROR +gtk_icon_theme_get_default +gtk_icon_theme_get_example_icon_name +gtk_icon_theme_get_for_screen +gtk_icon_theme_get_icon_sizes +gtk_icon_theme_get_search_path +gtk_icon_theme_has_icon +gtk_icon_theme_list_contexts +gtk_icon_theme_list_icons +gtk_icon_theme_load_icon +gtk_icon_theme_lookup_by_gicon +gtk_icon_theme_lookup_icon +gtk_icon_theme_new +gtk_icon_theme_prepend_search_path +gtk_icon_theme_rescan_if_needed +gtk_icon_theme_set_custom_theme +gtk_icon_theme_set_screen +gtk_icon_theme_set_search_path +GtkIconView +gtk_icon_view_convert_widget_to_bin_window_coords +gtk_icon_view_create_drag_icon +gtk_icon_view_enable_model_drag_dest +gtk_icon_view_enable_model_drag_source +GtkIconViewForeachFunc +gtk_icon_view_get_cell_rect +gtk_icon_view_get_columns +gtk_icon_view_get_column_spacing +gtk_icon_view_get_cursor +gtk_icon_view_get_dest_item_at_pos +gtk_icon_view_get_drag_dest_item +gtk_icon_view_get_item_at_pos +gtk_icon_view_get_item_column +gtk_icon_view_get_item_orientation +gtk_icon_view_get_item_padding +gtk_icon_view_get_item_row +gtk_icon_view_get_item_width +gtk_icon_view_get_margin +gtk_icon_view_get_markup_column +gtk_icon_view_get_model +gtk_icon_view_get_path_at_pos +gtk_icon_view_get_pixbuf_column +gtk_icon_view_get_reorderable +gtk_icon_view_get_row_spacing +gtk_icon_view_get_selected_items +gtk_icon_view_get_selection_mode +gtk_icon_view_get_spacing +gtk_icon_view_get_text_column +gtk_icon_view_get_tooltip_column +gtk_icon_view_get_tooltip_context +gtk_icon_view_get_visible_range +gtk_icon_view_item_activated +gtk_icon_view_new +gtk_icon_view_new_with_area +gtk_icon_view_new_with_model +gtk_icon_view_path_is_selected +gtk_icon_view_scroll_to_path +gtk_icon_view_select_all +gtk_icon_view_selected_foreach +gtk_icon_view_select_path +gtk_icon_view_set_columns +gtk_icon_view_set_column_spacing +gtk_icon_view_set_cursor +gtk_icon_view_set_drag_dest_item +gtk_icon_view_set_item_orientation +gtk_icon_view_set_item_padding +gtk_icon_view_set_item_width +gtk_icon_view_set_margin +gtk_icon_view_set_markup_column +gtk_icon_view_set_model +gtk_icon_view_set_pixbuf_column +gtk_icon_view_set_reorderable +gtk_icon_view_set_row_spacing +gtk_icon_view_set_selection_mode +gtk_icon_view_set_spacing +gtk_icon_view_set_text_column +gtk_icon_view_set_tooltip_cell +gtk_icon_view_set_tooltip_column +gtk_icon_view_set_tooltip_item +gtk_icon_view_unselect_all +gtk_icon_view_unselect_path +gtk_icon_view_unset_model_drag_dest +gtk_icon_view_unset_model_drag_source +GtkImage +gtk_image_clear +gtk_image_get_animation +gtk_image_get_gicon +gtk_image_get_icon_name +gtk_image_get_icon_set +gtk_image_get_pixbuf +gtk_image_get_pixel_size +gtk_image_get_stock +gtk_image_get_storage_type +GtkImageMenuItem +gtk_image_menu_item_get_always_show_image +gtk_image_menu_item_get_image +gtk_image_menu_item_get_use_stock +gtk_image_menu_item_new +gtk_image_menu_item_new_from_stock +gtk_image_menu_item_new_with_label +gtk_image_menu_item_new_with_mnemonic +gtk_image_menu_item_set_accel_group +gtk_image_menu_item_set_always_show_image +gtk_image_menu_item_set_image +gtk_image_menu_item_set_use_stock +gtk_image_new +gtk_image_new_from_animation +gtk_image_new_from_file +gtk_image_new_from_gicon +gtk_image_new_from_icon_name +gtk_image_new_from_icon_set +gtk_image_new_from_pixbuf +gtk_image_new_from_resource +gtk_image_new_from_stock +gtk_image_set_from_animation +gtk_image_set_from_file +gtk_image_set_from_gicon +gtk_image_set_from_icon_name +gtk_image_set_from_icon_set +gtk_image_set_from_pixbuf +gtk_image_set_from_resource +gtk_image_set_from_stock +gtk_image_set_pixel_size +GtkIMContext +GtkIMContextClass +gtk_im_context_delete_surrounding +gtk_im_context_filter_keypress +gtk_im_context_focus_in +gtk_im_context_focus_out +gtk_im_context_get_preedit_string +gtk_im_context_get_surrounding +GtkIMContextInfo +gtk_im_context_reset +gtk_im_context_set_client_window +gtk_im_context_set_cursor_location +gtk_im_context_set_surrounding +gtk_im_context_set_use_preedit +GtkIMContextSimple +gtk_im_context_simple_add_table +gtk_im_context_simple_new +GtkIMMulticontext +gtk_im_multicontext_append_menuitems +gtk_im_multicontext_get_context_id +gtk_im_multicontext_new +gtk_im_multicontext_set_context_id +GtkInfoBar +gtk_info_bar_add_action_widget +gtk_info_bar_add_button +gtk_info_bar_add_buttons +gtk_info_bar_get_action_area +gtk_info_bar_get_content_area +gtk_info_bar_get_message_type +gtk_info_bar_new +gtk_info_bar_new_with_buttons +gtk_info_bar_response +gtk_info_bar_set_default_response +gtk_info_bar_set_message_type +gtk_info_bar_set_response_sensitive +gtk_init +gtk_init_check +gtk_init_with_args +GTK_INPUT_ERROR +GTK_INTERFACE_AGE +GtkInvisible +gtk_invisible_get_screen +gtk_invisible_new +gtk_invisible_new_for_screen +gtk_invisible_set_screen +GTK_IS_RESIZE_CONTAINER +gtk_key_snooper_install +gtk_key_snooper_remove +GtkKeySnoopFunc +GtkLabel +gtk_label_get_angle +gtk_label_get_attributes +gtk_label_get_current_uri +gtk_label_get_ellipsize +gtk_label_get_justify +gtk_label_get_label +gtk_label_get_layout +gtk_label_get_layout_offsets +gtk_label_get_line_wrap +gtk_label_get_line_wrap_mode +gtk_label_get_max_width_chars +gtk_label_get_mnemonic_keyval +gtk_label_get_mnemonic_widget +gtk_label_get_selectable +gtk_label_get_selection_bounds +gtk_label_get_single_line_mode +gtk_label_get_text +gtk_label_get_track_visited_links +gtk_label_get_use_markup +gtk_label_get_use_underline +gtk_label_get_width_chars +gtk_label_new +gtk_label_new_with_mnemonic +gtk_label_select_region +gtk_label_set_angle +gtk_label_set_attributes +gtk_label_set_ellipsize +gtk_label_set_justify +gtk_label_set_label +gtk_label_set_line_wrap +gtk_label_set_line_wrap_mode +gtk_label_set_markup +gtk_label_set_markup_with_mnemonic +gtk_label_set_max_width_chars +gtk_label_set_mnemonic_widget +gtk_label_set_pattern +gtk_label_set_selectable +gtk_label_set_single_line_mode +gtk_label_set_text +gtk_label_set_text_with_mnemonic +gtk_label_set_track_visited_links +gtk_label_set_use_markup +gtk_label_set_use_underline +gtk_label_set_width_chars +GtkLayout +gtk_layout_get_bin_window +gtk_layout_get_hadjustment +gtk_layout_get_size +gtk_layout_get_vadjustment +gtk_layout_move +gtk_layout_new +gtk_layout_put +gtk_layout_set_hadjustment +gtk_layout_set_size +gtk_layout_set_vadjustment +GtkLevelBar +gtk_level_bar_add_offset_value +gtk_level_bar_get_inverted +gtk_level_bar_get_max_value +gtk_level_bar_get_min_value +gtk_level_bar_get_mode +gtk_level_bar_get_offset_value +gtk_level_bar_get_value +gtk_level_bar_new +gtk_level_bar_new_for_interval +GTK_LEVEL_BAR_OFFSET_HIGH +GTK_LEVEL_BAR_OFFSET_LOW +gtk_level_bar_remove_offset_value +gtk_level_bar_set_inverted +gtk_level_bar_set_max_value +gtk_level_bar_set_min_value +gtk_level_bar_set_mode +gtk_level_bar_set_value +GtkLinkButton +gtk_link_button_get_uri +gtk_link_button_get_visited +gtk_link_button_new +gtk_link_button_new_with_label +gtk_link_button_set_uri +gtk_link_button_set_visited +GtkListStore +gtk_list_store_append +gtk_list_store_clear +gtk_list_store_insert +gtk_list_store_insert_after +gtk_list_store_insert_before +gtk_list_store_insert_with_values +gtk_list_store_insert_with_valuesv +gtk_list_store_iter_is_valid +gtk_list_store_move_after +gtk_list_store_move_before +gtk_list_store_new +gtk_list_store_newv +gtk_list_store_prepend +gtk_list_store_remove +gtk_list_store_reorder +gtk_list_store_set +gtk_list_store_set_column_types +gtk_list_store_set_valist +gtk_list_store_set_value +gtk_list_store_set_valuesv +gtk_list_store_swap +GtkLockButton +gtk_lock_button_get_permission +gtk_lock_button_new +gtk_lock_button_set_permission +gtk_main +gtk_main_do_event +gtk_main_iteration +gtk_main_iteration_do +gtk_main_level +gtk_main_quit +GTK_MAJOR_VERSION +GTK_MAX_COMPOSE_LEN +GtkMenu +gtk_menu_attach +gtk_menu_attach_to_widget +GtkMenuBar +gtk_menu_bar_get_child_pack_direction +gtk_menu_bar_get_pack_direction +gtk_menu_bar_new +gtk_menu_bar_new_from_model +gtk_menu_bar_set_child_pack_direction +gtk_menu_bar_set_pack_direction +GtkMenuButton +gtk_menu_button_get_align_widget +gtk_menu_button_get_direction +gtk_menu_button_get_menu_model +gtk_menu_button_get_popup +gtk_menu_button_new +gtk_menu_button_set_align_widget +gtk_menu_button_set_direction +gtk_menu_button_set_menu_model +gtk_menu_button_set_popup +gtk_menu_detach +GtkMenuDetachFunc +gtk_menu_get_accel_group +gtk_menu_get_accel_path +gtk_menu_get_active +gtk_menu_get_attach_widget +gtk_menu_get_for_attach_widget +gtk_menu_get_monitor +gtk_menu_get_reserve_toggle_size +gtk_menu_get_tearoff_state +gtk_menu_get_title +GtkMenuItem +gtk_menu_item_activate +gtk_menu_item_deselect +gtk_menu_item_get_accel_path +gtk_menu_item_get_label +gtk_menu_item_get_reserve_indicator +gtk_menu_item_get_right_justified +gtk_menu_item_get_submenu +gtk_menu_item_get_use_underline +gtk_menu_item_new +gtk_menu_item_new_with_label +gtk_menu_item_new_with_mnemonic +gtk_menu_item_select +gtk_menu_item_set_accel_path +gtk_menu_item_set_label +gtk_menu_item_set_reserve_indicator +gtk_menu_item_set_right_justified +gtk_menu_item_set_submenu +gtk_menu_item_set_use_underline +gtk_menu_item_toggle_size_allocate +gtk_menu_item_toggle_size_request +gtk_menu_new +gtk_menu_new_from_model +gtk_menu_popdown +gtk_menu_popup +gtk_menu_popup_for_device +GtkMenuPositionFunc +gtk_menu_reorder_child +gtk_menu_reposition +gtk_menu_set_accel_group +gtk_menu_set_accel_path +gtk_menu_set_active +gtk_menu_set_monitor +gtk_menu_set_reserve_toggle_size +gtk_menu_set_screen +gtk_menu_set_tearoff_state +gtk_menu_set_title +GtkMenuShell +gtk_menu_shell_activate_item +gtk_menu_shell_append +gtk_menu_shell_bind_model +gtk_menu_shell_cancel +gtk_menu_shell_deactivate +gtk_menu_shell_deselect +gtk_menu_shell_get_parent_shell +gtk_menu_shell_get_selected_item +gtk_menu_shell_get_take_focus +gtk_menu_shell_insert +gtk_menu_shell_prepend +gtk_menu_shell_select_first +gtk_menu_shell_select_item +gtk_menu_shell_set_take_focus +GtkMenuToolButton +gtk_menu_tool_button_get_menu +gtk_menu_tool_button_new +gtk_menu_tool_button_new_from_stock +gtk_menu_tool_button_set_arrow_tooltip_markup +gtk_menu_tool_button_set_arrow_tooltip_text +gtk_menu_tool_button_set_menu +GtkMessageDialog +gtk_message_dialog_format_secondary_markup +gtk_message_dialog_format_secondary_text +gtk_message_dialog_get_image +gtk_message_dialog_get_message_area +gtk_message_dialog_new +gtk_message_dialog_new_with_markup +gtk_message_dialog_set_image +gtk_message_dialog_set_markup +GTK_MICRO_VERSION +GTK_MINOR_VERSION +GtkMisc +gtk_misc_get_alignment +gtk_misc_get_padding +gtk_misc_set_alignment +gtk_misc_set_padding +GtkModuleDisplayInitFunc +GtkModuleInitFunc +GtkMountOperation +gtk_mount_operation_get_parent +gtk_mount_operation_get_screen +gtk_mount_operation_is_showing +gtk_mount_operation_new +gtk_mount_operation_set_parent +gtk_mount_operation_set_screen +GtkNotebook +gtk_notebook_append_page +gtk_notebook_append_page_menu +gtk_notebook_get_action_widget +gtk_notebook_get_current_page +gtk_notebook_get_group_name +gtk_notebook_get_menu_label +gtk_notebook_get_menu_label_text +gtk_notebook_get_n_pages +gtk_notebook_get_nth_page +gtk_notebook_get_scrollable +gtk_notebook_get_show_border +gtk_notebook_get_show_tabs +gtk_notebook_get_tab_detachable +gtk_notebook_get_tab_hborder +gtk_notebook_get_tab_label +gtk_notebook_get_tab_label_text +gtk_notebook_get_tab_pos +gtk_notebook_get_tab_reorderable +gtk_notebook_get_tab_vborder +gtk_notebook_insert_page +gtk_notebook_insert_page_menu +gtk_notebook_new +gtk_notebook_next_page +gtk_notebook_page_num +gtk_notebook_popup_disable +gtk_notebook_popup_enable +gtk_notebook_prepend_page +gtk_notebook_prepend_page_menu +gtk_notebook_prev_page +gtk_notebook_remove_page +gtk_notebook_reorder_child +gtk_notebook_set_action_widget +gtk_notebook_set_current_page +gtk_notebook_set_group_name +gtk_notebook_set_menu_label +gtk_notebook_set_menu_label_text +gtk_notebook_set_scrollable +gtk_notebook_set_show_border +gtk_notebook_set_show_tabs +gtk_notebook_set_tab_detachable +gtk_notebook_set_tab_label +gtk_notebook_set_tab_label_text +gtk_notebook_set_tab_pos +gtk_notebook_set_tab_reorderable +GtkNumerableIcon +gtk_numerable_icon_get_background_gicon +gtk_numerable_icon_get_background_icon_name +gtk_numerable_icon_get_count +gtk_numerable_icon_get_label +gtk_numerable_icon_get_style_context +gtk_numerable_icon_new +gtk_numerable_icon_new_with_style_context +gtk_numerable_icon_set_background_gicon +gtk_numerable_icon_set_background_icon_name +gtk_numerable_icon_set_count +gtk_numerable_icon_set_label +gtk_numerable_icon_set_style_context +GtkOffscreenWindow +gtk_offscreen_window_get_pixbuf +gtk_offscreen_window_get_surface +gtk_offscreen_window_new +GtkOrientable +gtk_orientable_get_orientation +gtk_orientable_set_orientation +GtkOverlay +gtk_overlay_add_overlay +gtk_overlay_new +GtkPageRange +GtkPageSetup +gtk_page_setup_copy +GtkPageSetupDoneFunc +gtk_page_setup_get_bottom_margin +gtk_page_setup_get_left_margin +gtk_page_setup_get_orientation +gtk_page_setup_get_page_height +gtk_page_setup_get_page_width +gtk_page_setup_get_paper_height +gtk_page_setup_get_paper_size +gtk_page_setup_get_paper_width +gtk_page_setup_get_right_margin +gtk_page_setup_get_top_margin +gtk_page_setup_load_file +gtk_page_setup_load_key_file +gtk_page_setup_new +gtk_page_setup_new_from_file +gtk_page_setup_new_from_key_file +gtk_page_setup_set_bottom_margin +gtk_page_setup_set_left_margin +gtk_page_setup_set_orientation +gtk_page_setup_set_paper_size +gtk_page_setup_set_paper_size_and_default_margins +gtk_page_setup_set_right_margin +gtk_page_setup_set_top_margin +gtk_page_setup_to_file +gtk_page_setup_to_key_file +GtkPageSetupUnixDialog +gtk_page_setup_unix_dialog_get_page_setup +gtk_page_setup_unix_dialog_get_print_settings +gtk_page_setup_unix_dialog_new +gtk_page_setup_unix_dialog_set_page_setup +gtk_page_setup_unix_dialog_set_print_settings +gtk_paint_arrow +gtk_paint_box +gtk_paint_box_gap +gtk_paint_check +gtk_paint_diamond +gtk_paint_expander +gtk_paint_extension +gtk_paint_flat_box +gtk_paint_focus +gtk_paint_handle +gtk_paint_hline +gtk_paint_layout +gtk_paint_option +gtk_paint_resize_grip +gtk_paint_shadow +gtk_paint_shadow_gap +gtk_paint_slider +gtk_paint_spinner +gtk_paint_tab +gtk_paint_vline +GtkPaned +gtk_paned_add1 +gtk_paned_add2 +gtk_paned_get_child1 +gtk_paned_get_child2 +gtk_paned_get_handle_window +gtk_paned_get_position +gtk_paned_new +gtk_paned_pack1 +gtk_paned_pack2 +gtk_paned_set_position +GTK_PAPER_NAME_A3 +GTK_PAPER_NAME_A4 +GTK_PAPER_NAME_A5 +GTK_PAPER_NAME_B5 +GTK_PAPER_NAME_EXECUTIVE +GTK_PAPER_NAME_LEGAL +GTK_PAPER_NAME_LETTER +GtkPaperSize +gtk_paper_size_copy +gtk_paper_size_free +gtk_paper_size_get_default +gtk_paper_size_get_default_bottom_margin +gtk_paper_size_get_default_left_margin +gtk_paper_size_get_default_right_margin +gtk_paper_size_get_default_top_margin +gtk_paper_size_get_display_name +gtk_paper_size_get_height +gtk_paper_size_get_name +gtk_paper_size_get_paper_sizes +gtk_paper_size_get_ppd_name +gtk_paper_size_get_width +gtk_paper_size_is_custom +gtk_paper_size_is_equal +gtk_paper_size_new +gtk_paper_size_new_custom +gtk_paper_size_new_from_key_file +gtk_paper_size_new_from_ppd +gtk_paper_size_set_size +gtk_paper_size_to_key_file +gtk_parse_args +GtkPlug +gtk_plug_construct +gtk_plug_construct_for_display +gtk_plug_get_embedded +gtk_plug_get_id +gtk_plug_get_socket_window +gtk_plug_new +gtk_plug_new_for_display +GtkPrintBackend +GtkPrintContext +gtk_print_context_create_pango_context +gtk_print_context_create_pango_layout +gtk_print_context_get_cairo_context +gtk_print_context_get_dpi_x +gtk_print_context_get_dpi_y +gtk_print_context_get_hard_margins +gtk_print_context_get_height +gtk_print_context_get_page_setup +gtk_print_context_get_pango_fontmap +gtk_print_context_get_width +gtk_print_context_set_cairo_context +GtkPrinter +gtk_printer_accepts_pdf +gtk_printer_accepts_ps +gtk_printer_compare +GtkPrinterFunc +gtk_printer_get_backend +gtk_printer_get_capabilities +gtk_printer_get_default_page_size +gtk_printer_get_description +gtk_printer_get_hard_margins +gtk_printer_get_icon_name +gtk_printer_get_job_count +gtk_printer_get_location +gtk_printer_get_name +gtk_printer_get_state_message +gtk_printer_has_details +gtk_printer_is_accepting_jobs +gtk_printer_is_active +gtk_printer_is_default +gtk_printer_is_paused +gtk_printer_is_virtual +gtk_printer_list_papers +gtk_printer_new +gtk_printer_request_details +GTK_PRINT_ERROR +GtkPrintJob +GtkPrintJobCompleteFunc +gtk_print_job_get_collate +gtk_print_job_get_num_copies +gtk_print_job_get_n_up +gtk_print_job_get_n_up_layout +gtk_print_job_get_page_ranges +gtk_print_job_get_pages +gtk_print_job_get_page_set +gtk_print_job_get_printer +gtk_print_job_get_reverse +gtk_print_job_get_rotate +gtk_print_job_get_scale +gtk_print_job_get_settings +gtk_print_job_get_status +gtk_print_job_get_surface +gtk_print_job_get_title +gtk_print_job_get_track_print_status +gtk_print_job_new +gtk_print_job_send +gtk_print_job_set_collate +gtk_print_job_set_num_copies +gtk_print_job_set_n_up +gtk_print_job_set_n_up_layout +gtk_print_job_set_page_ranges +gtk_print_job_set_pages +gtk_print_job_set_page_set +gtk_print_job_set_reverse +gtk_print_job_set_rotate +gtk_print_job_set_scale +gtk_print_job_set_source_file +gtk_print_job_set_track_print_status +GtkPrintOperation +gtk_print_operation_cancel +gtk_print_operation_draw_page_finish +gtk_print_operation_get_default_page_setup +gtk_print_operation_get_embed_page_setup +gtk_print_operation_get_error +gtk_print_operation_get_has_selection +gtk_print_operation_get_n_pages_to_print +gtk_print_operation_get_print_settings +gtk_print_operation_get_status +gtk_print_operation_get_status_string +gtk_print_operation_get_support_selection +gtk_print_operation_is_finished +gtk_print_operation_new +GtkPrintOperationPreview +gtk_print_operation_preview_end_preview +gtk_print_operation_preview_is_selected +gtk_print_operation_preview_render_page +gtk_print_operation_run +gtk_print_operation_set_allow_async +gtk_print_operation_set_current_page +gtk_print_operation_set_custom_tab_label +gtk_print_operation_set_default_page_setup +gtk_print_operation_set_defer_drawing +gtk_print_operation_set_embed_page_setup +gtk_print_operation_set_export_filename +gtk_print_operation_set_has_selection +gtk_print_operation_set_job_name +gtk_print_operation_set_n_pages +gtk_print_operation_set_print_settings +gtk_print_operation_set_show_progress +gtk_print_operation_set_support_selection +gtk_print_operation_set_track_print_status +gtk_print_operation_set_unit +gtk_print_operation_set_use_full_page +gtk_print_run_page_setup_dialog +gtk_print_run_page_setup_dialog_async +GtkPrintSettings +GTK_PRINT_SETTINGS_COLLATE +gtk_print_settings_copy +GTK_PRINT_SETTINGS_DEFAULT_SOURCE +GTK_PRINT_SETTINGS_DITHER +GTK_PRINT_SETTINGS_DUPLEX +GTK_PRINT_SETTINGS_FINISHINGS +gtk_print_settings_foreach +GtkPrintSettingsFunc +gtk_print_settings_get +gtk_print_settings_get_bool +gtk_print_settings_get_collate +gtk_print_settings_get_default_source +gtk_print_settings_get_dither +gtk_print_settings_get_double +gtk_print_settings_get_double_with_default +gtk_print_settings_get_duplex +gtk_print_settings_get_finishings +gtk_print_settings_get_int +gtk_print_settings_get_int_with_default +gtk_print_settings_get_length +gtk_print_settings_get_media_type +gtk_print_settings_get_n_copies +gtk_print_settings_get_number_up +gtk_print_settings_get_number_up_layout +gtk_print_settings_get_orientation +gtk_print_settings_get_output_bin +gtk_print_settings_get_page_ranges +gtk_print_settings_get_page_set +gtk_print_settings_get_paper_height +gtk_print_settings_get_paper_size +gtk_print_settings_get_paper_width +gtk_print_settings_get_printer +gtk_print_settings_get_printer_lpi +gtk_print_settings_get_print_pages +gtk_print_settings_get_quality +gtk_print_settings_get_resolution +gtk_print_settings_get_resolution_x +gtk_print_settings_get_resolution_y +gtk_print_settings_get_reverse +gtk_print_settings_get_scale +gtk_print_settings_get_use_color +gtk_print_settings_has_key +gtk_print_settings_load_file +gtk_print_settings_load_key_file +GTK_PRINT_SETTINGS_MEDIA_TYPE +GTK_PRINT_SETTINGS_N_COPIES +gtk_print_settings_new +gtk_print_settings_new_from_file +gtk_print_settings_new_from_key_file +GTK_PRINT_SETTINGS_NUMBER_UP +GTK_PRINT_SETTINGS_NUMBER_UP_LAYOUT +GTK_PRINT_SETTINGS_ORIENTATION +GTK_PRINT_SETTINGS_OUTPUT_BIN +GTK_PRINT_SETTINGS_OUTPUT_FILE_FORMAT +GTK_PRINT_SETTINGS_OUTPUT_URI +GTK_PRINT_SETTINGS_PAGE_RANGES +GTK_PRINT_SETTINGS_PAGE_SET +GTK_PRINT_SETTINGS_PAPER_FORMAT +GTK_PRINT_SETTINGS_PAPER_HEIGHT +GTK_PRINT_SETTINGS_PAPER_WIDTH +GTK_PRINT_SETTINGS_PRINTER +GTK_PRINT_SETTINGS_PRINTER_LPI +GTK_PRINT_SETTINGS_PRINT_PAGES +GTK_PRINT_SETTINGS_QUALITY +GTK_PRINT_SETTINGS_RESOLUTION +GTK_PRINT_SETTINGS_RESOLUTION_X +GTK_PRINT_SETTINGS_RESOLUTION_Y +GTK_PRINT_SETTINGS_REVERSE +GTK_PRINT_SETTINGS_SCALE +gtk_print_settings_set +gtk_print_settings_set_bool +gtk_print_settings_set_collate +gtk_print_settings_set_default_source +gtk_print_settings_set_dither +gtk_print_settings_set_double +gtk_print_settings_set_duplex +gtk_print_settings_set_finishings +gtk_print_settings_set_int +gtk_print_settings_set_length +gtk_print_settings_set_media_type +gtk_print_settings_set_n_copies +gtk_print_settings_set_number_up +gtk_print_settings_set_number_up_layout +gtk_print_settings_set_orientation +gtk_print_settings_set_output_bin +gtk_print_settings_set_page_ranges +gtk_print_settings_set_page_set +gtk_print_settings_set_paper_height +gtk_print_settings_set_paper_size +gtk_print_settings_set_paper_width +gtk_print_settings_set_printer +gtk_print_settings_set_printer_lpi +gtk_print_settings_set_print_pages +gtk_print_settings_set_quality +gtk_print_settings_set_resolution +gtk_print_settings_set_resolution_xy +gtk_print_settings_set_reverse +gtk_print_settings_set_scale +gtk_print_settings_set_use_color +gtk_print_settings_to_file +gtk_print_settings_to_key_file +gtk_print_settings_unset +GTK_PRINT_SETTINGS_USE_COLOR +GTK_PRINT_SETTINGS_WIN32_DRIVER_EXTRA +GTK_PRINT_SETTINGS_WIN32_DRIVER_VERSION +GtkPrintUnixDialog +gtk_print_unix_dialog_add_custom_tab +gtk_print_unix_dialog_get_current_page +gtk_print_unix_dialog_get_embed_page_setup +gtk_print_unix_dialog_get_has_selection +gtk_print_unix_dialog_get_manual_capabilities +gtk_print_unix_dialog_get_page_setup +gtk_print_unix_dialog_get_page_setup_set +gtk_print_unix_dialog_get_selected_printer +gtk_print_unix_dialog_get_settings +gtk_print_unix_dialog_get_support_selection +gtk_print_unix_dialog_new +gtk_print_unix_dialog_set_current_page +gtk_print_unix_dialog_set_embed_page_setup +gtk_print_unix_dialog_set_has_selection +gtk_print_unix_dialog_set_manual_capabilities +gtk_print_unix_dialog_set_page_setup +gtk_print_unix_dialog_set_settings +gtk_print_unix_dialog_set_support_selection +GTK_PRIORITY_RESIZE +GtkProgressBar +gtk_progress_bar_get_ellipsize +gtk_progress_bar_get_fraction +gtk_progress_bar_get_inverted +gtk_progress_bar_get_pulse_step +gtk_progress_bar_get_show_text +gtk_progress_bar_get_text +gtk_progress_bar_new +gtk_progress_bar_pulse +gtk_progress_bar_set_ellipsize +gtk_progress_bar_set_fraction +gtk_progress_bar_set_inverted +gtk_progress_bar_set_pulse_step +gtk_progress_bar_set_show_text +gtk_progress_bar_set_text +gtk_propagate_event +GtkRadioAction +GtkRadioActionEntry +gtk_radio_action_get_current_value +gtk_radio_action_get_group +gtk_radio_action_join_group +gtk_radio_action_new +gtk_radio_action_set_current_value +gtk_radio_action_set_group +GtkRadioButton +gtk_radio_button_get_group +gtk_radio_button_join_group +gtk_radio_button_new +gtk_radio_button_new_from_widget +gtk_radio_button_new_with_label +gtk_radio_button_new_with_label_from_widget +gtk_radio_button_new_with_mnemonic +gtk_radio_button_new_with_mnemonic_from_widget +gtk_radio_button_set_group +GtkRadioMenuItem +gtk_radio_menu_item_get_group +gtk_radio_menu_item_new +gtk_radio_menu_item_new_from_widget +gtk_radio_menu_item_new_with_label +gtk_radio_menu_item_new_with_label_from_widget +gtk_radio_menu_item_new_with_mnemonic +gtk_radio_menu_item_new_with_mnemonic_from_widget +gtk_radio_menu_item_set_group +GtkRadioToolButton +gtk_radio_tool_button_get_group +gtk_radio_tool_button_new +gtk_radio_tool_button_new_from_stock +gtk_radio_tool_button_new_from_widget +gtk_radio_tool_button_new_with_stock_from_widget +gtk_radio_tool_button_set_group +GtkRange +gtk_range_get_adjustment +gtk_range_get_fill_level +gtk_range_get_flippable +gtk_range_get_inverted +gtk_range_get_lower_stepper_sensitivity +gtk_range_get_min_slider_size +gtk_range_get_range_rect +gtk_range_get_restrict_to_fill_level +gtk_range_get_round_digits +gtk_range_get_show_fill_level +gtk_range_get_slider_range +gtk_range_get_slider_size_fixed +gtk_range_get_upper_stepper_sensitivity +gtk_range_get_value +gtk_range_set_adjustment +gtk_range_set_fill_level +gtk_range_set_flippable +gtk_range_set_increments +gtk_range_set_inverted +gtk_range_set_lower_stepper_sensitivity +gtk_range_set_min_slider_size +gtk_range_set_range +gtk_range_set_restrict_to_fill_level +gtk_range_set_round_digits +gtk_range_set_show_fill_level +gtk_range_set_slider_size_fixed +gtk_range_set_upper_stepper_sensitivity +gtk_range_set_value +gtk_rc_add_default_file +gtk_rc_find_module_in_path +gtk_rc_find_pixmap_in_path +gtk_rc_get_default_files +gtk_rc_get_im_module_file +gtk_rc_get_im_module_path +gtk_rc_get_module_dir +gtk_rc_get_style +gtk_rc_get_style_by_paths +gtk_rc_get_theme_dir +gtk_rc_parse +gtk_rc_parse_color +gtk_rc_parse_color_full +gtk_rc_parse_priority +gtk_rc_parse_state +gtk_rc_parse_string +GtkRcProperty +gtk_rc_property_parse_border +gtk_rc_property_parse_color +gtk_rc_property_parse_enum +gtk_rc_property_parse_flags +GtkRcPropertyParser +gtk_rc_property_parse_requisition +gtk_rc_reparse_all +gtk_rc_reparse_all_for_settings +gtk_rc_reset_styles +gtk_rc_scanner_new +gtk_rc_set_default_files +GtkRcStyle +gtk_rc_style_copy +gtk_rc_style_new +GtkRecentAction +gtk_recent_action_get_show_numbers +gtk_recent_action_new +gtk_recent_action_new_for_manager +gtk_recent_action_set_show_numbers +GtkRecentChooser +gtk_recent_chooser_add_filter +GtkRecentChooserDialog +gtk_recent_chooser_dialog_new +gtk_recent_chooser_dialog_new_for_manager +GTK_RECENT_CHOOSER_ERROR +gtk_recent_chooser_get_current_item +gtk_recent_chooser_get_current_uri +gtk_recent_chooser_get_filter +gtk_recent_chooser_get_items +gtk_recent_chooser_get_limit +gtk_recent_chooser_get_local_only +gtk_recent_chooser_get_select_multiple +gtk_recent_chooser_get_show_icons +gtk_recent_chooser_get_show_not_found +gtk_recent_chooser_get_show_private +gtk_recent_chooser_get_show_tips +gtk_recent_chooser_get_sort_type +gtk_recent_chooser_get_uris +GtkRecentChooserIface +gtk_recent_chooser_list_filters +GtkRecentChooserMenu +gtk_recent_chooser_menu_get_show_numbers +gtk_recent_chooser_menu_new +gtk_recent_chooser_menu_new_for_manager +gtk_recent_chooser_menu_set_show_numbers +gtk_recent_chooser_remove_filter +gtk_recent_chooser_select_all +gtk_recent_chooser_select_uri +gtk_recent_chooser_set_current_uri +gtk_recent_chooser_set_filter +gtk_recent_chooser_set_limit +gtk_recent_chooser_set_local_only +gtk_recent_chooser_set_select_multiple +gtk_recent_chooser_set_show_icons +gtk_recent_chooser_set_show_not_found +gtk_recent_chooser_set_show_private +gtk_recent_chooser_set_show_tips +gtk_recent_chooser_set_sort_func +gtk_recent_chooser_set_sort_type +gtk_recent_chooser_unselect_all +gtk_recent_chooser_unselect_uri +GtkRecentChooserWidget +gtk_recent_chooser_widget_new +gtk_recent_chooser_widget_new_for_manager +GtkRecentData +GtkRecentFilter +gtk_recent_filter_add_age +gtk_recent_filter_add_application +gtk_recent_filter_add_custom +gtk_recent_filter_add_group +gtk_recent_filter_add_mime_type +gtk_recent_filter_add_pattern +gtk_recent_filter_add_pixbuf_formats +gtk_recent_filter_filter +GtkRecentFilterFunc +gtk_recent_filter_get_name +gtk_recent_filter_get_needed +GtkRecentFilterInfo +gtk_recent_filter_new +gtk_recent_filter_set_name +GtkRecentInfo +gtk_recent_info_create_app_info +gtk_recent_info_exists +gtk_recent_info_get_added +gtk_recent_info_get_age +gtk_recent_info_get_application_info +gtk_recent_info_get_applications +gtk_recent_info_get_description +gtk_recent_info_get_display_name +gtk_recent_info_get_gicon +gtk_recent_info_get_groups +gtk_recent_info_get_icon +gtk_recent_info_get_mime_type +gtk_recent_info_get_modified +gtk_recent_info_get_private_hint +gtk_recent_info_get_short_name +gtk_recent_info_get_uri +gtk_recent_info_get_uri_display +gtk_recent_info_get_visited +gtk_recent_info_has_application +gtk_recent_info_has_group +gtk_recent_info_is_local +gtk_recent_info_last_application +gtk_recent_info_match +gtk_recent_info_ref +gtk_recent_info_unref +GtkRecentManager +gtk_recent_manager_add_full +gtk_recent_manager_add_item +GTK_RECENT_MANAGER_ERROR +gtk_recent_manager_get_default +gtk_recent_manager_get_items +gtk_recent_manager_has_item +gtk_recent_manager_lookup_item +gtk_recent_manager_move_item +gtk_recent_manager_new +gtk_recent_manager_purge_items +gtk_recent_manager_remove_item +GtkRecentSortFunc +gtk_render_activity +gtk_render_arrow +gtk_render_background +gtk_render_check +gtk_render_expander +gtk_render_extension +gtk_render_focus +gtk_render_frame +gtk_render_frame_gap +gtk_render_handle +gtk_render_icon +gtk_render_icon_pixbuf +gtk_render_insertion_cursor +gtk_render_layout +gtk_render_line +gtk_render_option +gtk_render_slider +GtkRequestedSize +GtkRequisition +gtk_requisition_copy +gtk_requisition_free +gtk_requisition_new +gtk_rgb_to_hsv +GtkScale +gtk_scale_add_mark +GtkScaleButton +gtk_scale_button_get_adjustment +gtk_scale_button_get_minus_button +gtk_scale_button_get_plus_button +gtk_scale_button_get_popup +gtk_scale_button_get_value +gtk_scale_button_new +gtk_scale_button_set_adjustment +gtk_scale_button_set_icons +gtk_scale_button_set_value +gtk_scale_clear_marks +gtk_scale_get_digits +gtk_scale_get_draw_value +gtk_scale_get_has_origin +gtk_scale_get_layout +gtk_scale_get_layout_offsets +gtk_scale_get_value_pos +gtk_scale_new +gtk_scale_new_with_range +gtk_scale_set_digits +gtk_scale_set_draw_value +gtk_scale_set_has_origin +gtk_scale_set_value_pos +GtkScrollable +gtk_scrollable_get_hadjustment +gtk_scrollable_get_hscroll_policy +gtk_scrollable_get_vadjustment +gtk_scrollable_get_vscroll_policy +gtk_scrollable_set_hadjustment +gtk_scrollable_set_hscroll_policy +gtk_scrollable_set_vadjustment +gtk_scrollable_set_vscroll_policy +GtkScrollbar +gtk_scrollbar_new +GtkScrolledWindow +gtk_scrolled_window_add_with_viewport +gtk_scrolled_window_get_capture_button_press +gtk_scrolled_window_get_hadjustment +gtk_scrolled_window_get_hscrollbar +gtk_scrolled_window_get_kinetic_scrolling +gtk_scrolled_window_get_min_content_height +gtk_scrolled_window_get_min_content_width +gtk_scrolled_window_get_placement +gtk_scrolled_window_get_policy +gtk_scrolled_window_get_shadow_type +gtk_scrolled_window_get_vadjustment +gtk_scrolled_window_get_vscrollbar +gtk_scrolled_window_new +gtk_scrolled_window_set_capture_button_press +gtk_scrolled_window_set_hadjustment +gtk_scrolled_window_set_kinetic_scrolling +gtk_scrolled_window_set_min_content_height +gtk_scrolled_window_set_min_content_width +gtk_scrolled_window_set_placement +gtk_scrolled_window_set_policy +gtk_scrolled_window_set_shadow_type +gtk_scrolled_window_set_vadjustment +gtk_scrolled_window_unset_placement +GtkSearchEntry +gtk_search_entry_new +gtk_selection_add_target +gtk_selection_add_targets +gtk_selection_clear_targets +gtk_selection_convert +GtkSelectionData +gtk_selection_data_copy +gtk_selection_data_free +gtk_selection_data_get_data +gtk_selection_data_get_data_type +gtk_selection_data_get_data_with_length +gtk_selection_data_get_display +gtk_selection_data_get_format +gtk_selection_data_get_length +gtk_selection_data_get_pixbuf +gtk_selection_data_get_selection +gtk_selection_data_get_target +gtk_selection_data_get_targets +gtk_selection_data_get_text +gtk_selection_data_get_uris +gtk_selection_data_set +gtk_selection_data_set_pixbuf +gtk_selection_data_set_text +gtk_selection_data_set_uris +gtk_selection_data_targets_include_image +gtk_selection_data_targets_include_rich_text +gtk_selection_data_targets_include_text +gtk_selection_data_targets_include_uri +gtk_selection_owner_set +gtk_selection_owner_set_for_display +gtk_selection_remove_all +GtkSeparator +GtkSeparatorMenuItem +gtk_separator_menu_item_new +gtk_separator_new +GtkSeparatorToolItem +gtk_separator_tool_item_get_draw +gtk_separator_tool_item_new +gtk_separator_tool_item_set_draw +GtkSettings +gtk_settings_get_default +gtk_settings_get_for_screen +gtk_settings_install_property +gtk_settings_install_property_parser +gtk_settings_set_double_property +gtk_settings_set_long_property +gtk_settings_set_property_value +gtk_settings_set_string_property +GtkSettingsValue +gtk_show_about_dialog +gtk_show_uri +GtkSizeGroup +gtk_size_group_add_widget +gtk_size_group_get_ignore_hidden +gtk_size_group_get_mode +gtk_size_group_get_widgets +gtk_size_group_new +gtk_size_group_remove_widget +gtk_size_group_set_ignore_hidden +gtk_size_group_set_mode +GtkSocket +gtk_socket_add_id +gtk_socket_get_id +gtk_socket_get_plug_window +gtk_socket_new +GtkSpinButton +gtk_spin_button_configure +gtk_spin_button_get_adjustment +gtk_spin_button_get_digits +gtk_spin_button_get_increments +gtk_spin_button_get_numeric +gtk_spin_button_get_range +gtk_spin_button_get_snap_to_ticks +gtk_spin_button_get_update_policy +gtk_spin_button_get_value +gtk_spin_button_get_value_as_int +gtk_spin_button_get_wrap +gtk_spin_button_new +gtk_spin_button_new_with_range +gtk_spin_button_set_adjustment +gtk_spin_button_set_digits +gtk_spin_button_set_increments +gtk_spin_button_set_numeric +gtk_spin_button_set_range +gtk_spin_button_set_snap_to_ticks +gtk_spin_button_set_update_policy +gtk_spin_button_set_value +gtk_spin_button_set_wrap +gtk_spin_button_spin +gtk_spin_button_update +GtkSpinner +gtk_spinner_new +gtk_spinner_start +gtk_spinner_stop +GtkStatusbar +gtk_statusbar_get_context_id +gtk_statusbar_get_message_area +gtk_statusbar_new +gtk_statusbar_pop +gtk_statusbar_push +gtk_statusbar_remove +gtk_statusbar_remove_all +GtkStatusIcon +gtk_status_icon_get_geometry +gtk_status_icon_get_gicon +gtk_status_icon_get_has_tooltip +gtk_status_icon_get_icon_name +gtk_status_icon_get_pixbuf +gtk_status_icon_get_screen +gtk_status_icon_get_size +gtk_status_icon_get_stock +gtk_status_icon_get_storage_type +gtk_status_icon_get_title +gtk_status_icon_get_tooltip_markup +gtk_status_icon_get_tooltip_text +gtk_status_icon_get_visible +gtk_status_icon_get_x11_window_id +gtk_status_icon_is_embedded +gtk_status_icon_new +gtk_status_icon_new_from_file +gtk_status_icon_new_from_gicon +gtk_status_icon_new_from_icon_name +gtk_status_icon_new_from_pixbuf +gtk_status_icon_new_from_stock +gtk_status_icon_position_menu +gtk_status_icon_set_from_file +gtk_status_icon_set_from_gicon +gtk_status_icon_set_from_icon_name +gtk_status_icon_set_from_pixbuf +gtk_status_icon_set_from_stock +gtk_status_icon_set_has_tooltip +gtk_status_icon_set_name +gtk_status_icon_set_screen +gtk_status_icon_set_title +gtk_status_icon_set_tooltip_markup +gtk_status_icon_set_tooltip_text +gtk_status_icon_set_visible +GTK_STOCK_ABOUT +gtk_stock_add +GTK_STOCK_ADD +gtk_stock_add_static +GTK_STOCK_APPLY +GTK_STOCK_BOLD +GTK_STOCK_CANCEL +GTK_STOCK_CAPS_LOCK_WARNING +GTK_STOCK_CDROM +GTK_STOCK_CLEAR +GTK_STOCK_CLOSE +GTK_STOCK_COLOR_PICKER +GTK_STOCK_CONNECT +GTK_STOCK_CONVERT +GTK_STOCK_COPY +GTK_STOCK_CUT +GTK_STOCK_DELETE +GTK_STOCK_DIALOG_AUTHENTICATION +GTK_STOCK_DIALOG_ERROR +GTK_STOCK_DIALOG_INFO +GTK_STOCK_DIALOG_QUESTION +GTK_STOCK_DIALOG_WARNING +GTK_STOCK_DIRECTORY +GTK_STOCK_DISCARD +GTK_STOCK_DISCONNECT +GTK_STOCK_DND +GTK_STOCK_DND_MULTIPLE +GTK_STOCK_EDIT +GTK_STOCK_EXECUTE +GTK_STOCK_FILE +GTK_STOCK_FIND +GTK_STOCK_FIND_AND_REPLACE +GTK_STOCK_FLOPPY +GTK_STOCK_FULLSCREEN +GTK_STOCK_GO_BACK +GTK_STOCK_GO_DOWN +GTK_STOCK_GO_FORWARD +GTK_STOCK_GOTO_BOTTOM +GTK_STOCK_GOTO_FIRST +GTK_STOCK_GOTO_LAST +GTK_STOCK_GOTO_TOP +GTK_STOCK_GO_UP +GTK_STOCK_HARDDISK +GTK_STOCK_HELP +GTK_STOCK_HOME +GTK_STOCK_INDENT +GTK_STOCK_INDEX +GTK_STOCK_INFO +GTK_STOCK_ITALIC +GtkStockItem +gtk_stock_item_copy +gtk_stock_item_free +GTK_STOCK_JUMP_TO +GTK_STOCK_JUSTIFY_CENTER +GTK_STOCK_JUSTIFY_FILL +GTK_STOCK_JUSTIFY_LEFT +GTK_STOCK_JUSTIFY_RIGHT +GTK_STOCK_LEAVE_FULLSCREEN +gtk_stock_list_ids +gtk_stock_lookup +GTK_STOCK_MEDIA_FORWARD +GTK_STOCK_MEDIA_NEXT +GTK_STOCK_MEDIA_PAUSE +GTK_STOCK_MEDIA_PLAY +GTK_STOCK_MEDIA_PREVIOUS +GTK_STOCK_MEDIA_RECORD +GTK_STOCK_MEDIA_REWIND +GTK_STOCK_MEDIA_STOP +GTK_STOCK_MISSING_IMAGE +GTK_STOCK_NETWORK +GTK_STOCK_NEW +GTK_STOCK_NO +GTK_STOCK_OK +GTK_STOCK_OPEN +GTK_STOCK_ORIENTATION_LANDSCAPE +GTK_STOCK_ORIENTATION_PORTRAIT +GTK_STOCK_ORIENTATION_REVERSE_LANDSCAPE +GTK_STOCK_ORIENTATION_REVERSE_PORTRAIT +GTK_STOCK_PAGE_SETUP +GTK_STOCK_PASTE +GTK_STOCK_PREFERENCES +GTK_STOCK_PRINT +GTK_STOCK_PRINT_ERROR +GTK_STOCK_PRINT_PAUSED +GTK_STOCK_PRINT_PREVIEW +GTK_STOCK_PRINT_REPORT +GTK_STOCK_PRINT_WARNING +GTK_STOCK_PROPERTIES +GTK_STOCK_QUIT +GTK_STOCK_REDO +GTK_STOCK_REFRESH +GTK_STOCK_REMOVE +GTK_STOCK_REVERT_TO_SAVED +GTK_STOCK_SAVE +GTK_STOCK_SAVE_AS +GTK_STOCK_SELECT_ALL +GTK_STOCK_SELECT_COLOR +GTK_STOCK_SELECT_FONT +gtk_stock_set_translate_func +GTK_STOCK_SORT_ASCENDING +GTK_STOCK_SORT_DESCENDING +GTK_STOCK_SPELL_CHECK +GTK_STOCK_STOP +GTK_STOCK_STRIKETHROUGH +GTK_STOCK_UNDELETE +GTK_STOCK_UNDERLINE +GTK_STOCK_UNDO +GTK_STOCK_UNINDENT +GTK_STOCK_YES +GTK_STOCK_ZOOM_100 +GTK_STOCK_ZOOM_FIT +GTK_STOCK_ZOOM_IN +GTK_STOCK_ZOOM_OUT +GtkStyle +gtk_style_apply_default_background +gtk_style_attach +GTK_STYLE_ATTACHED +GTK_STYLE_CLASS_ACCELERATOR +GTK_STYLE_CLASS_ARROW +GTK_STYLE_CLASS_BACKGROUND +GTK_STYLE_CLASS_BOTTOM +GTK_STYLE_CLASS_BUTTON +GTK_STYLE_CLASS_CALENDAR +GTK_STYLE_CLASS_CELL +GTK_STYLE_CLASS_CHECK +GTK_STYLE_CLASS_COMBOBOX_ENTRY +GTK_STYLE_CLASS_CURSOR_HANDLE +GTK_STYLE_CLASS_DEFAULT +GTK_STYLE_CLASS_DND +GTK_STYLE_CLASS_DOCK +GTK_STYLE_CLASS_ENTRY +GTK_STYLE_CLASS_ERROR +GTK_STYLE_CLASS_EXPANDER +GTK_STYLE_CLASS_FRAME +GTK_STYLE_CLASS_GRIP +GTK_STYLE_CLASS_HEADER +GTK_STYLE_CLASS_HIGHLIGHT +GTK_STYLE_CLASS_HORIZONTAL +GTK_STYLE_CLASS_IMAGE +GTK_STYLE_CLASS_INFO +GTK_STYLE_CLASS_INLINE_TOOLBAR +GTK_STYLE_CLASS_INSERTION_CURSOR +GTK_STYLE_CLASS_LEFT +GTK_STYLE_CLASS_LEVEL_BAR +GTK_STYLE_CLASS_LINKED +GTK_STYLE_CLASS_MARK +GTK_STYLE_CLASS_MENU +GTK_STYLE_CLASS_MENUBAR +GTK_STYLE_CLASS_MENUITEM +GTK_STYLE_CLASS_NOTEBOOK +GTK_STYLE_CLASS_OSD +GTK_STYLE_CLASS_PANE_SEPARATOR +GTK_STYLE_CLASS_PRIMARY_TOOLBAR +GTK_STYLE_CLASS_PROGRESSBAR +GTK_STYLE_CLASS_QUESTION +GTK_STYLE_CLASS_RADIO +GTK_STYLE_CLASS_RIGHT +GTK_STYLE_CLASS_RUBBERBAND +GTK_STYLE_CLASS_SCALE +GTK_STYLE_CLASS_SCALE_HAS_MARKS_ABOVE +GTK_STYLE_CLASS_SCALE_HAS_MARKS_BELOW +GTK_STYLE_CLASS_SCROLLBAR +GTK_STYLE_CLASS_SCROLLBARS_JUNCTION +GTK_STYLE_CLASS_SEPARATOR +GTK_STYLE_CLASS_SIDEBAR +GTK_STYLE_CLASS_SLIDER +GTK_STYLE_CLASS_SPINBUTTON +GTK_STYLE_CLASS_SPINNER +GTK_STYLE_CLASS_TOOLBAR +GTK_STYLE_CLASS_TOOLTIP +GTK_STYLE_CLASS_TOP +GTK_STYLE_CLASS_TROUGH +GTK_STYLE_CLASS_VERTICAL +GTK_STYLE_CLASS_VIEW +GTK_STYLE_CLASS_WARNING +GtkStyleContext +gtk_style_context_add_class +gtk_style_context_add_provider +gtk_style_context_add_provider_for_screen +gtk_style_context_add_region +gtk_style_context_cancel_animations +gtk_style_context_get +gtk_style_context_get_background_color +gtk_style_context_get_border +gtk_style_context_get_border_color +gtk_style_context_get_color +gtk_style_context_get_direction +gtk_style_context_get_font +gtk_style_context_get_junction_sides +gtk_style_context_get_margin +gtk_style_context_get_padding +gtk_style_context_get_parent +gtk_style_context_get_path +gtk_style_context_get_property +gtk_style_context_get_screen +gtk_style_context_get_section +gtk_style_context_get_state +gtk_style_context_get_style +gtk_style_context_get_style_property +gtk_style_context_get_style_valist +gtk_style_context_get_valist +gtk_style_context_has_class +gtk_style_context_has_region +gtk_style_context_invalidate +gtk_style_context_list_classes +gtk_style_context_list_regions +gtk_style_context_lookup_color +gtk_style_context_lookup_icon_set +gtk_style_context_new +gtk_style_context_notify_state_change +gtk_style_context_pop_animatable_region +gtk_style_context_push_animatable_region +gtk_style_context_remove_class +gtk_style_context_remove_provider +gtk_style_context_remove_provider_for_screen +gtk_style_context_remove_region +gtk_style_context_reset_widgets +gtk_style_context_restore +gtk_style_context_save +gtk_style_context_scroll_animations +gtk_style_context_set_background +gtk_style_context_set_direction +gtk_style_context_set_junction_sides +gtk_style_context_set_parent +gtk_style_context_set_path +gtk_style_context_set_screen +gtk_style_context_set_state +gtk_style_context_state_is_running +gtk_style_copy +gtk_style_detach +gtk_style_get +gtk_style_get_style_property +gtk_style_get_valist +gtk_style_has_context +gtk_style_lookup_color +gtk_style_lookup_icon_set +gtk_style_new +GtkStyleProperties +gtk_style_properties_clear +gtk_style_properties_get +gtk_style_properties_get_property +gtk_style_properties_get_valist +gtk_style_properties_lookup_color +gtk_style_properties_lookup_property +gtk_style_properties_map_color +gtk_style_properties_merge +gtk_style_properties_new +gtk_style_properties_register_property +gtk_style_properties_set +gtk_style_properties_set_property +gtk_style_properties_set_valist +gtk_style_properties_unset_property +GTK_STYLE_PROPERTY_BACKGROUND_COLOR +GTK_STYLE_PROPERTY_BACKGROUND_IMAGE +GTK_STYLE_PROPERTY_BORDER_COLOR +GTK_STYLE_PROPERTY_BORDER_RADIUS +GTK_STYLE_PROPERTY_BORDER_STYLE +GTK_STYLE_PROPERTY_BORDER_WIDTH +GTK_STYLE_PROPERTY_COLOR +GTK_STYLE_PROPERTY_FONT +GTK_STYLE_PROPERTY_MARGIN +GTK_STYLE_PROPERTY_PADDING +GtkStylePropertyParser +GtkStyleProvider +gtk_style_provider_get_icon_factory +gtk_style_provider_get_style +gtk_style_provider_get_style_property +GtkStyleProviderIface +GTK_STYLE_PROVIDER_PRIORITY_APPLICATION +GTK_STYLE_PROVIDER_PRIORITY_FALLBACK +GTK_STYLE_PROVIDER_PRIORITY_SETTINGS +GTK_STYLE_PROVIDER_PRIORITY_THEME +GTK_STYLE_PROVIDER_PRIORITY_USER +GTK_STYLE_REGION_COLUMN +GTK_STYLE_REGION_COLUMN_HEADER +GTK_STYLE_REGION_ROW +GTK_STYLE_REGION_TAB +gtk_style_render_icon +gtk_style_set_background +GtkSwitch +gtk_switch_get_active +gtk_switch_new +gtk_switch_set_active +GtkSymbolicColor +gtk_symbolic_color_new_alpha +gtk_symbolic_color_new_literal +gtk_symbolic_color_new_mix +gtk_symbolic_color_new_name +gtk_symbolic_color_new_shade +gtk_symbolic_color_new_win32 +gtk_symbolic_color_ref +gtk_symbolic_color_resolve +gtk_symbolic_color_to_string +gtk_symbolic_color_unref +GtkTable +gtk_table_attach +gtk_table_attach_defaults +gtk_table_get_col_spacing +gtk_table_get_default_col_spacing +gtk_table_get_default_row_spacing +gtk_table_get_homogeneous +gtk_table_get_row_spacing +gtk_table_get_size +gtk_table_new +gtk_table_resize +gtk_table_set_col_spacing +gtk_table_set_col_spacings +gtk_table_set_homogeneous +gtk_table_set_row_spacing +gtk_table_set_row_spacings +GtkTargetEntry +gtk_target_entry_copy +gtk_target_entry_free +gtk_target_entry_new +GtkTargetList +gtk_target_list_add +gtk_target_list_add_image_targets +gtk_target_list_add_rich_text_targets +gtk_target_list_add_table +gtk_target_list_add_text_targets +gtk_target_list_add_uri_targets +gtk_target_list_find +gtk_target_list_new +gtk_target_list_ref +gtk_target_list_remove +gtk_target_list_unref +gtk_targets_include_image +gtk_targets_include_rich_text +gtk_targets_include_text +gtk_targets_include_uri +gtk_target_table_free +gtk_target_table_new_from_list +GtkTearoffMenuItem +gtk_tearoff_menu_item_new +gtk_test_create_simple_window +gtk_test_create_widget +gtk_test_display_button_window +gtk_test_find_label +gtk_test_find_sibling +gtk_test_find_widget +gtk_test_init +gtk_test_list_all_types +gtk_test_register_all_types +gtk_test_slider_get_value +gtk_test_slider_set_perc +gtk_test_spin_button_click +gtk_test_text_get +gtk_test_text_set +gtk_test_widget_click +gtk_test_widget_send_key +GtkTextAppearance +GtkTextAttributes +gtk_text_attributes_copy +gtk_text_attributes_copy_values +gtk_text_attributes_new +gtk_text_attributes_ref +gtk_text_attributes_unref +GtkTextBuffer +gtk_text_buffer_add_mark +gtk_text_buffer_add_selection_clipboard +gtk_text_buffer_apply_tag +gtk_text_buffer_apply_tag_by_name +gtk_text_buffer_backspace +gtk_text_buffer_begin_user_action +gtk_text_buffer_copy_clipboard +gtk_text_buffer_create_child_anchor +gtk_text_buffer_create_mark +gtk_text_buffer_create_tag +gtk_text_buffer_cut_clipboard +gtk_text_buffer_delete +gtk_text_buffer_delete_interactive +gtk_text_buffer_delete_mark +gtk_text_buffer_delete_mark_by_name +gtk_text_buffer_delete_selection +gtk_text_buffer_deserialize +GtkTextBufferDeserializeFunc +gtk_text_buffer_deserialize_get_can_create_tags +gtk_text_buffer_deserialize_set_can_create_tags +gtk_text_buffer_end_user_action +gtk_text_buffer_get_bounds +gtk_text_buffer_get_char_count +gtk_text_buffer_get_copy_target_list +gtk_text_buffer_get_deserialize_formats +gtk_text_buffer_get_end_iter +gtk_text_buffer_get_has_selection +gtk_text_buffer_get_insert +gtk_text_buffer_get_iter_at_child_anchor +gtk_text_buffer_get_iter_at_line +gtk_text_buffer_get_iter_at_line_index +gtk_text_buffer_get_iter_at_line_offset +gtk_text_buffer_get_iter_at_mark +gtk_text_buffer_get_iter_at_offset +gtk_text_buffer_get_line_count +gtk_text_buffer_get_mark +gtk_text_buffer_get_modified +gtk_text_buffer_get_paste_target_list +gtk_text_buffer_get_selection_bound +gtk_text_buffer_get_selection_bounds +gtk_text_buffer_get_serialize_formats +gtk_text_buffer_get_slice +gtk_text_buffer_get_start_iter +gtk_text_buffer_get_tag_table +gtk_text_buffer_get_text +gtk_text_buffer_insert +gtk_text_buffer_insert_at_cursor +gtk_text_buffer_insert_child_anchor +gtk_text_buffer_insert_interactive +gtk_text_buffer_insert_interactive_at_cursor +gtk_text_buffer_insert_pixbuf +gtk_text_buffer_insert_range +gtk_text_buffer_insert_range_interactive +gtk_text_buffer_insert_with_tags +gtk_text_buffer_insert_with_tags_by_name +gtk_text_buffer_move_mark +gtk_text_buffer_move_mark_by_name +gtk_text_buffer_new +gtk_text_buffer_paste_clipboard +gtk_text_buffer_place_cursor +gtk_text_buffer_register_deserialize_format +gtk_text_buffer_register_deserialize_tagset +gtk_text_buffer_register_serialize_format +gtk_text_buffer_register_serialize_tagset +gtk_text_buffer_remove_all_tags +gtk_text_buffer_remove_selection_clipboard +gtk_text_buffer_remove_tag +gtk_text_buffer_remove_tag_by_name +gtk_text_buffer_select_range +gtk_text_buffer_serialize +GtkTextBufferSerializeFunc +gtk_text_buffer_set_modified +gtk_text_buffer_set_text +gtk_text_buffer_unregister_deserialize_format +gtk_text_buffer_unregister_serialize_format +GtkTextCharPredicate +GtkTextChildAnchor +gtk_text_child_anchor_get_deleted +gtk_text_child_anchor_get_widgets +gtk_text_child_anchor_new +GtkTextIter +gtk_text_iter_assign +gtk_text_iter_backward_char +gtk_text_iter_backward_chars +gtk_text_iter_backward_cursor_position +gtk_text_iter_backward_cursor_positions +gtk_text_iter_backward_find_char +gtk_text_iter_backward_line +gtk_text_iter_backward_lines +gtk_text_iter_backward_search +gtk_text_iter_backward_sentence_start +gtk_text_iter_backward_sentence_starts +gtk_text_iter_backward_to_tag_toggle +gtk_text_iter_backward_visible_cursor_position +gtk_text_iter_backward_visible_cursor_positions +gtk_text_iter_backward_visible_line +gtk_text_iter_backward_visible_lines +gtk_text_iter_backward_visible_word_start +gtk_text_iter_backward_visible_word_starts +gtk_text_iter_backward_word_start +gtk_text_iter_backward_word_starts +gtk_text_iter_begins_tag +gtk_text_iter_can_insert +gtk_text_iter_compare +gtk_text_iter_copy +gtk_text_iter_editable +gtk_text_iter_ends_line +gtk_text_iter_ends_sentence +gtk_text_iter_ends_tag +gtk_text_iter_ends_word +gtk_text_iter_equal +gtk_text_iter_forward_char +gtk_text_iter_forward_chars +gtk_text_iter_forward_cursor_position +gtk_text_iter_forward_cursor_positions +gtk_text_iter_forward_find_char +gtk_text_iter_forward_line +gtk_text_iter_forward_lines +gtk_text_iter_forward_search +gtk_text_iter_forward_sentence_end +gtk_text_iter_forward_sentence_ends +gtk_text_iter_forward_to_end +gtk_text_iter_forward_to_line_end +gtk_text_iter_forward_to_tag_toggle +gtk_text_iter_forward_visible_cursor_position +gtk_text_iter_forward_visible_cursor_positions +gtk_text_iter_forward_visible_line +gtk_text_iter_forward_visible_lines +gtk_text_iter_forward_visible_word_end +gtk_text_iter_forward_visible_word_ends +gtk_text_iter_forward_word_end +gtk_text_iter_forward_word_ends +gtk_text_iter_free +gtk_text_iter_get_attributes +gtk_text_iter_get_buffer +gtk_text_iter_get_bytes_in_line +gtk_text_iter_get_char +gtk_text_iter_get_chars_in_line +gtk_text_iter_get_child_anchor +gtk_text_iter_get_language +gtk_text_iter_get_line +gtk_text_iter_get_line_index +gtk_text_iter_get_line_offset +gtk_text_iter_get_marks +gtk_text_iter_get_offset +gtk_text_iter_get_pixbuf +gtk_text_iter_get_slice +gtk_text_iter_get_tags +gtk_text_iter_get_text +gtk_text_iter_get_toggled_tags +gtk_text_iter_get_visible_line_index +gtk_text_iter_get_visible_line_offset +gtk_text_iter_get_visible_slice +gtk_text_iter_get_visible_text +gtk_text_iter_has_tag +gtk_text_iter_in_range +gtk_text_iter_inside_sentence +gtk_text_iter_inside_word +gtk_text_iter_is_cursor_position +gtk_text_iter_is_end +gtk_text_iter_is_start +gtk_text_iter_order +gtk_text_iter_set_line +gtk_text_iter_set_line_index +gtk_text_iter_set_line_offset +gtk_text_iter_set_offset +gtk_text_iter_set_visible_line_index +gtk_text_iter_set_visible_line_offset +gtk_text_iter_starts_line +gtk_text_iter_starts_sentence +gtk_text_iter_starts_word +gtk_text_iter_toggles_tag +GtkTextMark +gtk_text_mark_get_buffer +gtk_text_mark_get_deleted +gtk_text_mark_get_left_gravity +gtk_text_mark_get_name +gtk_text_mark_get_visible +gtk_text_mark_new +gtk_text_mark_set_visible +GtkTextTag +gtk_text_tag_event +gtk_text_tag_get_priority +gtk_text_tag_new +gtk_text_tag_set_priority +GtkTextTagTable +gtk_text_tag_table_add +gtk_text_tag_table_foreach +GtkTextTagTableForeach +gtk_text_tag_table_get_size +gtk_text_tag_table_lookup +gtk_text_tag_table_new +gtk_text_tag_table_remove +GtkTextView +gtk_text_view_add_child_at_anchor +gtk_text_view_add_child_in_window +gtk_text_view_backward_display_line +gtk_text_view_backward_display_line_start +gtk_text_view_buffer_to_window_coords +gtk_text_view_forward_display_line +gtk_text_view_forward_display_line_end +gtk_text_view_get_accepts_tab +gtk_text_view_get_border_window_size +gtk_text_view_get_buffer +gtk_text_view_get_cursor_locations +gtk_text_view_get_cursor_visible +gtk_text_view_get_default_attributes +gtk_text_view_get_editable +gtk_text_view_get_hadjustment +gtk_text_view_get_indent +gtk_text_view_get_input_hints +gtk_text_view_get_input_purpose +gtk_text_view_get_iter_at_location +gtk_text_view_get_iter_at_position +gtk_text_view_get_iter_location +gtk_text_view_get_justification +gtk_text_view_get_left_margin +gtk_text_view_get_line_at_y +gtk_text_view_get_line_yrange +gtk_text_view_get_overwrite +gtk_text_view_get_pixels_above_lines +gtk_text_view_get_pixels_below_lines +gtk_text_view_get_pixels_inside_wrap +gtk_text_view_get_right_margin +gtk_text_view_get_tabs +gtk_text_view_get_vadjustment +gtk_text_view_get_visible_rect +gtk_text_view_get_window +gtk_text_view_get_window_type +gtk_text_view_get_wrap_mode +gtk_text_view_im_context_filter_keypress +gtk_text_view_move_child +gtk_text_view_move_mark_onscreen +gtk_text_view_move_visually +gtk_text_view_new +gtk_text_view_new_with_buffer +gtk_text_view_place_cursor_onscreen +GTK_TEXT_VIEW_PRIORITY_VALIDATE +gtk_text_view_reset_im_context +gtk_text_view_scroll_mark_onscreen +gtk_text_view_scroll_to_iter +gtk_text_view_scroll_to_mark +gtk_text_view_set_accepts_tab +gtk_text_view_set_border_window_size +gtk_text_view_set_buffer +gtk_text_view_set_cursor_visible +gtk_text_view_set_editable +gtk_text_view_set_indent +gtk_text_view_set_input_hints +gtk_text_view_set_input_purpose +gtk_text_view_set_justification +gtk_text_view_set_left_margin +gtk_text_view_set_overwrite +gtk_text_view_set_pixels_above_lines +gtk_text_view_set_pixels_below_lines +gtk_text_view_set_pixels_inside_wrap +gtk_text_view_set_right_margin +gtk_text_view_set_tabs +gtk_text_view_set_wrap_mode +gtk_text_view_starts_display_line +gtk_text_view_window_to_buffer_coords +GtkThemingEngine +GtkThemingEngineClass +gtk_theming_engine_get +gtk_theming_engine_get_background_color +gtk_theming_engine_get_border +gtk_theming_engine_get_border_color +gtk_theming_engine_get_color +gtk_theming_engine_get_direction +gtk_theming_engine_get_font +gtk_theming_engine_get_junction_sides +gtk_theming_engine_get_margin +gtk_theming_engine_get_padding +gtk_theming_engine_get_path +gtk_theming_engine_get_property +gtk_theming_engine_get_screen +gtk_theming_engine_get_state +gtk_theming_engine_get_style +gtk_theming_engine_get_style_property +gtk_theming_engine_get_style_valist +gtk_theming_engine_get_valist +gtk_theming_engine_has_class +gtk_theming_engine_has_region +gtk_theming_engine_load +gtk_theming_engine_lookup_color +gtk_theming_engine_register_property +gtk_theming_engine_state_is_running +GtkToggleAction +GtkToggleActionEntry +gtk_toggle_action_get_active +gtk_toggle_action_get_draw_as_radio +gtk_toggle_action_new +gtk_toggle_action_set_active +gtk_toggle_action_set_draw_as_radio +gtk_toggle_action_toggled +GtkToggleButton +gtk_toggle_button_get_active +gtk_toggle_button_get_inconsistent +gtk_toggle_button_get_mode +gtk_toggle_button_new +gtk_toggle_button_new_with_label +gtk_toggle_button_new_with_mnemonic +gtk_toggle_button_set_active +gtk_toggle_button_set_inconsistent +gtk_toggle_button_set_mode +gtk_toggle_button_toggled +GtkToggleToolButton +gtk_toggle_tool_button_get_active +gtk_toggle_tool_button_new +gtk_toggle_tool_button_new_from_stock +gtk_toggle_tool_button_set_active +GtkToolbar +gtk_toolbar_get_drop_index +gtk_toolbar_get_icon_size +gtk_toolbar_get_item_index +gtk_toolbar_get_n_items +gtk_toolbar_get_nth_item +gtk_toolbar_get_relief_style +gtk_toolbar_get_show_arrow +gtk_toolbar_get_style +gtk_toolbar_insert +gtk_toolbar_new +gtk_toolbar_set_drop_highlight_item +gtk_toolbar_set_icon_size +gtk_toolbar_set_show_arrow +gtk_toolbar_set_style +gtk_toolbar_unset_icon_size +gtk_toolbar_unset_style +GtkToolButton +gtk_tool_button_get_icon_name +gtk_tool_button_get_icon_widget +gtk_tool_button_get_label +gtk_tool_button_get_label_widget +gtk_tool_button_get_stock_id +gtk_tool_button_get_use_underline +gtk_tool_button_new +gtk_tool_button_new_from_stock +gtk_tool_button_set_icon_name +gtk_tool_button_set_icon_widget +gtk_tool_button_set_label +gtk_tool_button_set_label_widget +gtk_tool_button_set_stock_id +gtk_tool_button_set_use_underline +GtkToolItem +gtk_tool_item_get_ellipsize_mode +gtk_tool_item_get_expand +gtk_tool_item_get_homogeneous +gtk_tool_item_get_icon_size +gtk_tool_item_get_is_important +gtk_tool_item_get_orientation +gtk_tool_item_get_proxy_menu_item +gtk_tool_item_get_relief_style +gtk_tool_item_get_text_alignment +gtk_tool_item_get_text_orientation +gtk_tool_item_get_text_size_group +gtk_tool_item_get_toolbar_style +gtk_tool_item_get_use_drag_window +gtk_tool_item_get_visible_horizontal +gtk_tool_item_get_visible_vertical +GtkToolItemGroup +gtk_tool_item_group_get_collapsed +gtk_tool_item_group_get_drop_item +gtk_tool_item_group_get_ellipsize +gtk_tool_item_group_get_header_relief +gtk_tool_item_group_get_item_position +gtk_tool_item_group_get_label +gtk_tool_item_group_get_label_widget +gtk_tool_item_group_get_n_items +gtk_tool_item_group_get_nth_item +gtk_tool_item_group_insert +gtk_tool_item_group_new +gtk_tool_item_group_set_collapsed +gtk_tool_item_group_set_ellipsize +gtk_tool_item_group_set_header_relief +gtk_tool_item_group_set_item_position +gtk_tool_item_group_set_label +gtk_tool_item_group_set_label_widget +gtk_tool_item_new +gtk_tool_item_rebuild_menu +gtk_tool_item_retrieve_proxy_menu_item +gtk_tool_item_set_expand +gtk_tool_item_set_homogeneous +gtk_tool_item_set_is_important +gtk_tool_item_set_proxy_menu_item +gtk_tool_item_set_tooltip_markup +gtk_tool_item_set_tooltip_text +gtk_tool_item_set_use_drag_window +gtk_tool_item_set_visible_horizontal +gtk_tool_item_set_visible_vertical +gtk_tool_item_toolbar_reconfigured +GtkToolPalette +gtk_tool_palette_add_drag_dest +gtk_tool_palette_get_drag_item +gtk_tool_palette_get_drag_target_group +gtk_tool_palette_get_drag_target_item +gtk_tool_palette_get_drop_group +gtk_tool_palette_get_drop_item +gtk_tool_palette_get_exclusive +gtk_tool_palette_get_expand +gtk_tool_palette_get_group_position +gtk_tool_palette_get_hadjustment +gtk_tool_palette_get_icon_size +gtk_tool_palette_get_style +gtk_tool_palette_get_vadjustment +gtk_tool_palette_new +gtk_tool_palette_set_drag_source +gtk_tool_palette_set_exclusive +gtk_tool_palette_set_expand +gtk_tool_palette_set_group_position +gtk_tool_palette_set_icon_size +gtk_tool_palette_set_style +gtk_tool_palette_unset_icon_size +gtk_tool_palette_unset_style +GtkToolShell +gtk_tool_shell_get_ellipsize_mode +gtk_tool_shell_get_icon_size +gtk_tool_shell_get_orientation +gtk_tool_shell_get_relief_style +gtk_tool_shell_get_style +gtk_tool_shell_get_text_alignment +gtk_tool_shell_get_text_orientation +gtk_tool_shell_get_text_size_group +GtkToolShellIface +gtk_tool_shell_rebuild_menu +GtkTooltip +gtk_tooltip_set_custom +gtk_tooltip_set_icon +gtk_tooltip_set_icon_from_gicon +gtk_tooltip_set_icon_from_icon_name +gtk_tooltip_set_icon_from_stock +gtk_tooltip_set_markup +gtk_tooltip_set_text +gtk_tooltip_set_tip_area +gtk_tooltip_trigger_tooltip_query +GtkTranslateFunc +GtkTreeCellDataFunc +GtkTreeDestroyCountFunc +GtkTreeDragDest +gtk_tree_drag_dest_drag_data_received +GtkTreeDragDestIface +gtk_tree_drag_dest_row_drop_possible +GtkTreeDragSource +gtk_tree_drag_source_drag_data_delete +gtk_tree_drag_source_drag_data_get +GtkTreeDragSourceIface +gtk_tree_drag_source_row_draggable +gtk_tree_get_row_drag_data +GtkTreeIter +GtkTreeIterCompareFunc +gtk_tree_iter_copy +gtk_tree_iter_free +GtkTreeModel +GtkTreeModelFilter +gtk_tree_model_filter_clear_cache +gtk_tree_model_filter_convert_child_iter_to_iter +gtk_tree_model_filter_convert_child_path_to_path +gtk_tree_model_filter_convert_iter_to_child_iter +gtk_tree_model_filter_convert_path_to_child_path +gtk_tree_model_filter_get_model +GtkTreeModelFilterModifyFunc +gtk_tree_model_filter_new +gtk_tree_model_filter_refilter +gtk_tree_model_filter_set_modify_func +gtk_tree_model_filter_set_visible_column +gtk_tree_model_filter_set_visible_func +GtkTreeModelFilterVisibleFunc +gtk_tree_model_foreach +GtkTreeModelForeachFunc +gtk_tree_model_get +gtk_tree_model_get_column_type +gtk_tree_model_get_flags +gtk_tree_model_get_iter +gtk_tree_model_get_iter_first +gtk_tree_model_get_iter_from_string +gtk_tree_model_get_n_columns +gtk_tree_model_get_path +gtk_tree_model_get_string_from_iter +gtk_tree_model_get_valist +gtk_tree_model_get_value +GtkTreeModelIface +gtk_tree_model_iter_children +gtk_tree_model_iter_has_child +gtk_tree_model_iter_n_children +gtk_tree_model_iter_next +gtk_tree_model_iter_nth_child +gtk_tree_model_iter_parent +gtk_tree_model_iter_previous +gtk_tree_model_ref_node +gtk_tree_model_row_changed +gtk_tree_model_row_deleted +gtk_tree_model_row_has_child_toggled +gtk_tree_model_row_inserted +gtk_tree_model_rows_reordered +GtkTreeModelSort +gtk_tree_model_sort_clear_cache +gtk_tree_model_sort_convert_child_iter_to_iter +gtk_tree_model_sort_convert_child_path_to_path +gtk_tree_model_sort_convert_iter_to_child_iter +gtk_tree_model_sort_convert_path_to_child_path +gtk_tree_model_sort_get_model +gtk_tree_model_sort_iter_is_valid +gtk_tree_model_sort_new_with_model +gtk_tree_model_sort_reset_default_sort_func +gtk_tree_model_unref_node +GtkTreePath +gtk_tree_path_append_index +gtk_tree_path_compare +gtk_tree_path_copy +gtk_tree_path_down +gtk_tree_path_free +gtk_tree_path_get_depth +gtk_tree_path_get_indices +gtk_tree_path_get_indices_with_depth +gtk_tree_path_is_ancestor +gtk_tree_path_is_descendant +gtk_tree_path_new +gtk_tree_path_new_first +gtk_tree_path_new_from_indices +gtk_tree_path_new_from_string +gtk_tree_path_next +gtk_tree_path_prepend_index +gtk_tree_path_prev +gtk_tree_path_to_string +gtk_tree_path_up +GtkTreeRowReference +gtk_tree_row_reference_copy +gtk_tree_row_reference_deleted +gtk_tree_row_reference_free +gtk_tree_row_reference_get_model +gtk_tree_row_reference_get_path +gtk_tree_row_reference_inserted +gtk_tree_row_reference_new +gtk_tree_row_reference_new_proxy +gtk_tree_row_reference_reordered +gtk_tree_row_reference_valid +GtkTreeSelection +gtk_tree_selection_count_selected_rows +GtkTreeSelectionForeachFunc +GtkTreeSelectionFunc +gtk_tree_selection_get_mode +gtk_tree_selection_get_selected +gtk_tree_selection_get_selected_rows +gtk_tree_selection_get_select_function +gtk_tree_selection_get_tree_view +gtk_tree_selection_get_user_data +gtk_tree_selection_iter_is_selected +gtk_tree_selection_path_is_selected +gtk_tree_selection_select_all +gtk_tree_selection_selected_foreach +gtk_tree_selection_select_iter +gtk_tree_selection_select_path +gtk_tree_selection_select_range +gtk_tree_selection_set_mode +gtk_tree_selection_set_select_function +gtk_tree_selection_unselect_all +gtk_tree_selection_unselect_iter +gtk_tree_selection_unselect_path +gtk_tree_selection_unselect_range +gtk_tree_set_row_drag_data +GtkTreeSortable +gtk_tree_sortable_get_sort_column_id +gtk_tree_sortable_has_default_sort_func +GtkTreeSortableIface +gtk_tree_sortable_set_default_sort_func +gtk_tree_sortable_set_sort_column_id +gtk_tree_sortable_set_sort_func +gtk_tree_sortable_sort_column_changed +GtkTreeStore +gtk_tree_store_append +gtk_tree_store_clear +gtk_tree_store_insert +gtk_tree_store_insert_after +gtk_tree_store_insert_before +gtk_tree_store_insert_with_values +gtk_tree_store_insert_with_valuesv +gtk_tree_store_is_ancestor +gtk_tree_store_iter_depth +gtk_tree_store_iter_is_valid +gtk_tree_store_move_after +gtk_tree_store_move_before +gtk_tree_store_new +gtk_tree_store_newv +gtk_tree_store_prepend +gtk_tree_store_remove +gtk_tree_store_reorder +gtk_tree_store_set +gtk_tree_store_set_column_types +gtk_tree_store_set_valist +gtk_tree_store_set_value +gtk_tree_store_set_valuesv +gtk_tree_store_swap +GtkTreeView +gtk_tree_view_append_column +gtk_tree_view_collapse_all +gtk_tree_view_collapse_row +GtkTreeViewColumn +gtk_tree_view_column_add_attribute +gtk_tree_view_column_cell_get_position +gtk_tree_view_column_cell_get_size +gtk_tree_view_column_cell_is_visible +gtk_tree_view_column_cell_set_cell_data +gtk_tree_view_column_clear +gtk_tree_view_column_clear_attributes +gtk_tree_view_column_clicked +GtkTreeViewColumnDropFunc +gtk_tree_view_column_focus_cell +gtk_tree_view_column_get_alignment +gtk_tree_view_column_get_button +gtk_tree_view_column_get_clickable +gtk_tree_view_column_get_expand +gtk_tree_view_column_get_fixed_width +gtk_tree_view_column_get_max_width +gtk_tree_view_column_get_min_width +gtk_tree_view_column_get_reorderable +gtk_tree_view_column_get_resizable +gtk_tree_view_column_get_sizing +gtk_tree_view_column_get_sort_column_id +gtk_tree_view_column_get_sort_indicator +gtk_tree_view_column_get_sort_order +gtk_tree_view_column_get_spacing +gtk_tree_view_column_get_title +gtk_tree_view_column_get_tree_view +gtk_tree_view_column_get_visible +gtk_tree_view_column_get_widget +gtk_tree_view_column_get_width +gtk_tree_view_column_get_x_offset +gtk_tree_view_column_new +gtk_tree_view_column_new_with_area +gtk_tree_view_column_new_with_attributes +gtk_tree_view_column_pack_end +gtk_tree_view_column_pack_start +gtk_tree_view_column_queue_resize +gtk_tree_view_columns_autosize +gtk_tree_view_column_set_alignment +gtk_tree_view_column_set_attributes +gtk_tree_view_column_set_cell_data_func +gtk_tree_view_column_set_clickable +gtk_tree_view_column_set_expand +gtk_tree_view_column_set_fixed_width +gtk_tree_view_column_set_max_width +gtk_tree_view_column_set_min_width +gtk_tree_view_column_set_reorderable +gtk_tree_view_column_set_resizable +gtk_tree_view_column_set_sizing +gtk_tree_view_column_set_sort_column_id +gtk_tree_view_column_set_sort_indicator +gtk_tree_view_column_set_sort_order +gtk_tree_view_column_set_spacing +gtk_tree_view_column_set_title +gtk_tree_view_column_set_visible +gtk_tree_view_column_set_widget +gtk_tree_view_convert_bin_window_to_tree_coords +gtk_tree_view_convert_bin_window_to_widget_coords +gtk_tree_view_convert_tree_to_bin_window_coords +gtk_tree_view_convert_tree_to_widget_coords +gtk_tree_view_convert_widget_to_bin_window_coords +gtk_tree_view_convert_widget_to_tree_coords +gtk_tree_view_create_row_drag_icon +gtk_tree_view_enable_model_drag_dest +gtk_tree_view_enable_model_drag_source +gtk_tree_view_expand_all +gtk_tree_view_expand_row +gtk_tree_view_expand_to_path +gtk_tree_view_get_background_area +gtk_tree_view_get_bin_window +gtk_tree_view_get_cell_area +gtk_tree_view_get_column +gtk_tree_view_get_columns +gtk_tree_view_get_cursor +gtk_tree_view_get_dest_row_at_pos +gtk_tree_view_get_drag_dest_row +gtk_tree_view_get_enable_search +gtk_tree_view_get_enable_tree_lines +gtk_tree_view_get_expander_column +gtk_tree_view_get_fixed_height_mode +gtk_tree_view_get_grid_lines +gtk_tree_view_get_hadjustment +gtk_tree_view_get_headers_clickable +gtk_tree_view_get_headers_visible +gtk_tree_view_get_hover_expand +gtk_tree_view_get_hover_selection +gtk_tree_view_get_level_indentation +gtk_tree_view_get_model +gtk_tree_view_get_n_columns +gtk_tree_view_get_path_at_pos +gtk_tree_view_get_reorderable +gtk_tree_view_get_row_separator_func +gtk_tree_view_get_rubber_banding +gtk_tree_view_get_rules_hint +gtk_tree_view_get_search_column +gtk_tree_view_get_search_entry +gtk_tree_view_get_search_equal_func +gtk_tree_view_get_search_position_func +gtk_tree_view_get_selection +gtk_tree_view_get_show_expanders +gtk_tree_view_get_tooltip_column +gtk_tree_view_get_tooltip_context +gtk_tree_view_get_vadjustment +gtk_tree_view_get_visible_range +gtk_tree_view_get_visible_rect +gtk_tree_view_insert_column +gtk_tree_view_insert_column_with_attributes +gtk_tree_view_insert_column_with_data_func +gtk_tree_view_is_blank_at_pos +gtk_tree_view_is_rubber_banding_active +gtk_tree_view_map_expanded_rows +GtkTreeViewMappingFunc +gtk_tree_view_move_column_after +gtk_tree_view_new +gtk_tree_view_new_with_model +GtkTreeViewPrivate +gtk_tree_view_remove_column +gtk_tree_view_row_activated +gtk_tree_view_row_expanded +GtkTreeViewRowSeparatorFunc +gtk_tree_view_scroll_to_cell +gtk_tree_view_scroll_to_point +GtkTreeViewSearchEqualFunc +GtkTreeViewSearchPositionFunc +gtk_tree_view_set_column_drag_function +gtk_tree_view_set_cursor +gtk_tree_view_set_cursor_on_cell +gtk_tree_view_set_destroy_count_func +gtk_tree_view_set_drag_dest_row +gtk_tree_view_set_enable_search +gtk_tree_view_set_enable_tree_lines +gtk_tree_view_set_expander_column +gtk_tree_view_set_fixed_height_mode +gtk_tree_view_set_grid_lines +gtk_tree_view_set_hadjustment +gtk_tree_view_set_headers_clickable +gtk_tree_view_set_headers_visible +gtk_tree_view_set_hover_expand +gtk_tree_view_set_hover_selection +gtk_tree_view_set_level_indentation +gtk_tree_view_set_model +gtk_tree_view_set_reorderable +gtk_tree_view_set_row_separator_func +gtk_tree_view_set_rubber_banding +gtk_tree_view_set_rules_hint +gtk_tree_view_set_search_column +gtk_tree_view_set_search_entry +gtk_tree_view_set_search_equal_func +gtk_tree_view_set_search_position_func +gtk_tree_view_set_show_expanders +gtk_tree_view_set_tooltip_cell +gtk_tree_view_set_tooltip_column +gtk_tree_view_set_tooltip_row +gtk_tree_view_set_vadjustment +gtk_tree_view_unset_rows_drag_dest +gtk_tree_view_unset_rows_drag_source +gtk_true +GtkUIManager +gtk_ui_manager_add_ui +gtk_ui_manager_add_ui_from_file +gtk_ui_manager_add_ui_from_resource +gtk_ui_manager_add_ui_from_string +gtk_ui_manager_ensure_update +gtk_ui_manager_get_accel_group +gtk_ui_manager_get_action +gtk_ui_manager_get_action_groups +gtk_ui_manager_get_add_tearoffs +gtk_ui_manager_get_toplevels +gtk_ui_manager_get_ui +gtk_ui_manager_get_widget +gtk_ui_manager_insert_action_group +gtk_ui_manager_new +gtk_ui_manager_new_merge_id +gtk_ui_manager_remove_action_group +gtk_ui_manager_remove_ui +gtk_ui_manager_set_add_tearoffs +GtkVBox +gtk_vbox_new +GtkVButtonBox +gtk_vbutton_box_new +GtkViewport +gtk_viewport_get_bin_window +gtk_viewport_get_hadjustment +gtk_viewport_get_shadow_type +gtk_viewport_get_vadjustment +gtk_viewport_get_view_window +gtk_viewport_new +gtk_viewport_set_hadjustment +gtk_viewport_set_shadow_type +gtk_viewport_set_vadjustment +GtkVolumeButton +gtk_volume_button_new +GtkVPaned +gtk_vpaned_new +GtkVScale +gtk_vscale_new +gtk_vscale_new_with_range +GtkVScrollbar +gtk_vscrollbar_new +GtkVSeparator +gtk_vseparator_new +GtkWidget +gtk_widget_activate +gtk_widget_add_accelerator +gtk_widget_add_device_events +gtk_widget_add_events +gtk_widget_add_mnemonic_label +GtkWidgetAuxInfo +gtk_widget_can_activate_accel +gtk_widget_child_focus +gtk_widget_child_notify +GtkWidgetClass +gtk_widget_class_find_style_property +gtk_widget_class_install_style_property +gtk_widget_class_install_style_property_parser +gtk_widget_class_list_style_properties +gtk_widget_class_path +gtk_widget_class_set_accessible_role +gtk_widget_class_set_accessible_type +gtk_widget_compute_expand +gtk_widget_create_pango_context +gtk_widget_create_pango_layout +gtk_widget_destroy +gtk_widget_destroyed +gtk_widget_device_is_shadowed +gtk_widget_draw +gtk_widget_ensure_style +gtk_widget_error_bell +gtk_widget_event +gtk_widget_freeze_child_notify +gtk_widget_get_accessible +gtk_widget_get_allocated_height +gtk_widget_get_allocated_width +gtk_widget_get_allocation +gtk_widget_get_ancestor +gtk_widget_get_app_paintable +gtk_widget_get_can_default +gtk_widget_get_can_focus +gtk_widget_get_child_requisition +gtk_widget_get_child_visible +gtk_widget_get_clipboard +gtk_widget_get_composite_name +gtk_widget_get_default_direction +gtk_widget_get_default_style +gtk_widget_get_device_enabled +gtk_widget_get_device_events +gtk_widget_get_direction +gtk_widget_get_display +gtk_widget_get_double_buffered +gtk_widget_get_events +gtk_widget_get_halign +gtk_widget_get_has_tooltip +gtk_widget_get_has_window +gtk_widget_get_hexpand +gtk_widget_get_hexpand_set +gtk_widget_get_mapped +gtk_widget_get_margin_bottom +gtk_widget_get_margin_left +gtk_widget_get_margin_right +gtk_widget_get_margin_top +gtk_widget_get_modifier_mask +gtk_widget_get_modifier_style +gtk_widget_get_name +gtk_widget_get_no_show_all +gtk_widget_get_pango_context +gtk_widget_get_parent +gtk_widget_get_parent_window +gtk_widget_get_path +gtk_widget_get_pointer +gtk_widget_get_preferred_height +gtk_widget_get_preferred_height_for_width +gtk_widget_get_preferred_size +gtk_widget_get_preferred_width +gtk_widget_get_preferred_width_for_height +gtk_widget_get_realized +gtk_widget_get_receives_default +gtk_widget_get_request_mode +gtk_widget_get_requisition +gtk_widget_get_root_window +gtk_widget_get_screen +gtk_widget_get_sensitive +gtk_widget_get_settings +gtk_widget_get_size_request +gtk_widget_get_state +gtk_widget_get_state_flags +gtk_widget_get_style +gtk_widget_get_style_context +gtk_widget_get_support_multidevice +gtk_widget_get_tooltip_markup +gtk_widget_get_tooltip_text +gtk_widget_get_tooltip_window +gtk_widget_get_toplevel +gtk_widget_get_valign +gtk_widget_get_vexpand +gtk_widget_get_vexpand_set +gtk_widget_get_visible +gtk_widget_get_visual +gtk_widget_get_window +gtk_widget_grab_default +gtk_widget_grab_focus +gtk_widget_has_default +gtk_widget_has_focus +gtk_widget_has_grab +gtk_widget_has_rc_style +gtk_widget_has_screen +gtk_widget_has_visible_focus +gtk_widget_hide +gtk_widget_hide_on_delete +gtk_widget_in_destruction +gtk_widget_input_shape_combine_region +gtk_widget_insert_action_group +gtk_widget_intersect +gtk_widget_is_ancestor +gtk_widget_is_composited +gtk_widget_is_drawable +gtk_widget_is_focus +gtk_widget_is_sensitive +gtk_widget_is_toplevel +gtk_widget_is_visible +gtk_widget_keynav_failed +gtk_widget_list_accel_closures +gtk_widget_list_mnemonic_labels +gtk_widget_map +gtk_widget_mnemonic_activate +gtk_widget_modify_base +gtk_widget_modify_bg +gtk_widget_modify_cursor +gtk_widget_modify_fg +gtk_widget_modify_font +gtk_widget_modify_style +gtk_widget_modify_text +gtk_widget_new +gtk_widget_override_background_color +gtk_widget_override_color +gtk_widget_override_cursor +gtk_widget_override_font +gtk_widget_override_symbolic_color +gtk_widget_path +GtkWidgetPath +gtk_widget_path_append_for_widget +gtk_widget_path_append_type +gtk_widget_path_append_with_siblings +gtk_widget_path_copy +gtk_widget_path_free +gtk_widget_path_get_object_type +gtk_widget_path_has_parent +gtk_widget_path_is_type +gtk_widget_path_iter_add_class +gtk_widget_path_iter_add_region +gtk_widget_path_iter_clear_classes +gtk_widget_path_iter_clear_regions +gtk_widget_path_iter_get_name +gtk_widget_path_iter_get_object_type +gtk_widget_path_iter_get_sibling_index +gtk_widget_path_iter_get_siblings +gtk_widget_path_iter_has_class +gtk_widget_path_iter_has_name +gtk_widget_path_iter_has_qclass +gtk_widget_path_iter_has_qname +gtk_widget_path_iter_has_qregion +gtk_widget_path_iter_has_region +gtk_widget_path_iter_list_classes +gtk_widget_path_iter_list_regions +gtk_widget_path_iter_remove_class +gtk_widget_path_iter_remove_region +gtk_widget_path_iter_set_name +gtk_widget_path_iter_set_object_type +gtk_widget_path_length +gtk_widget_path_new +gtk_widget_path_prepend_type +gtk_widget_path_ref +gtk_widget_path_to_string +gtk_widget_path_unref +gtk_widget_pop_composite_child +gtk_widget_push_composite_child +gtk_widget_queue_compute_expand +gtk_widget_queue_draw +gtk_widget_queue_draw_area +gtk_widget_queue_draw_region +gtk_widget_queue_resize +gtk_widget_queue_resize_no_redraw +gtk_widget_realize +gtk_widget_region_intersect +gtk_widget_remove_accelerator +gtk_widget_remove_mnemonic_label +gtk_widget_render_icon +gtk_widget_render_icon_pixbuf +gtk_widget_reparent +gtk_widget_reset_rc_styles +gtk_widget_reset_style +gtk_widget_send_expose +gtk_widget_send_focus_change +gtk_widget_set_accel_path +gtk_widget_set_allocation +gtk_widget_set_app_paintable +gtk_widget_set_can_default +gtk_widget_set_can_focus +gtk_widget_set_child_visible +gtk_widget_set_composite_name +gtk_widget_set_default_direction +gtk_widget_set_device_enabled +gtk_widget_set_device_events +gtk_widget_set_direction +gtk_widget_set_double_buffered +gtk_widget_set_events +gtk_widget_set_halign +gtk_widget_set_has_tooltip +gtk_widget_set_has_window +gtk_widget_set_hexpand +gtk_widget_set_hexpand_set +gtk_widget_set_mapped +gtk_widget_set_margin_bottom +gtk_widget_set_margin_left +gtk_widget_set_margin_right +gtk_widget_set_margin_top +gtk_widget_set_name +gtk_widget_set_no_show_all +gtk_widget_set_parent +gtk_widget_set_parent_window +gtk_widget_set_realized +gtk_widget_set_receives_default +gtk_widget_set_redraw_on_allocate +gtk_widget_set_sensitive +gtk_widget_set_size_request +gtk_widget_set_state +gtk_widget_set_state_flags +gtk_widget_set_style +gtk_widget_set_support_multidevice +gtk_widget_set_tooltip_markup +gtk_widget_set_tooltip_text +gtk_widget_set_tooltip_window +gtk_widget_set_valign +gtk_widget_set_vexpand +gtk_widget_set_vexpand_set +gtk_widget_set_visible +gtk_widget_set_visual +gtk_widget_set_window +gtk_widget_shape_combine_region +gtk_widget_show +gtk_widget_show_all +gtk_widget_show_now +gtk_widget_size_allocate +gtk_widget_size_request +gtk_widget_style_attach +gtk_widget_style_get +gtk_widget_style_get_property +gtk_widget_style_get_valist +gtk_widget_thaw_child_notify +gtk_widget_translate_coordinates +gtk_widget_trigger_tooltip_query +gtk_widget_unmap +gtk_widget_unparent +gtk_widget_unrealize +gtk_widget_unset_state_flags +GtkWindow +gtk_window_activate_default +gtk_window_activate_focus +gtk_window_activate_key +gtk_window_add_accel_group +gtk_window_add_mnemonic +gtk_window_begin_move_drag +gtk_window_begin_resize_drag +gtk_window_deiconify +gtk_window_fullscreen +gtk_window_get_accept_focus +gtk_window_get_application +gtk_window_get_attached_to +gtk_window_get_decorated +gtk_window_get_default_icon_list +gtk_window_get_default_icon_name +gtk_window_get_default_size +gtk_window_get_default_widget +gtk_window_get_deletable +gtk_window_get_destroy_with_parent +gtk_window_get_focus +gtk_window_get_focus_on_map +gtk_window_get_focus_visible +gtk_window_get_gravity +gtk_window_get_group +gtk_window_get_has_resize_grip +gtk_window_get_hide_titlebar_when_maximized +gtk_window_get_icon +gtk_window_get_icon_list +gtk_window_get_icon_name +gtk_window_get_mnemonic_modifier +gtk_window_get_mnemonics_visible +gtk_window_get_modal +gtk_window_get_opacity +gtk_window_get_position +gtk_window_get_resizable +gtk_window_get_resize_grip_area +gtk_window_get_role +gtk_window_get_screen +gtk_window_get_size +gtk_window_get_skip_pager_hint +gtk_window_get_skip_taskbar_hint +gtk_window_get_title +gtk_window_get_transient_for +gtk_window_get_type_hint +gtk_window_get_urgency_hint +gtk_window_get_window_type +GtkWindowGroup +gtk_window_group_add_window +gtk_window_group_get_current_device_grab +gtk_window_group_get_current_grab +gtk_window_group_list_windows +gtk_window_group_new +gtk_window_group_remove_window +gtk_window_has_group +gtk_window_has_toplevel_focus +gtk_window_iconify +gtk_window_is_active +gtk_window_list_toplevels +gtk_window_maximize +gtk_window_mnemonic_activate +gtk_window_move +gtk_window_new +gtk_window_parse_geometry +gtk_window_present +gtk_window_present_with_time +gtk_window_propagate_key_event +gtk_window_remove_accel_group +gtk_window_remove_mnemonic +gtk_window_reshow_with_initial_size +gtk_window_resize +gtk_window_resize_grip_is_visible +gtk_window_resize_to_geometry +gtk_window_set_accept_focus +gtk_window_set_application +gtk_window_set_attached_to +gtk_window_set_auto_startup_notification +gtk_window_set_decorated +gtk_window_set_default +gtk_window_set_default_geometry +gtk_window_set_default_icon +gtk_window_set_default_icon_from_file +gtk_window_set_default_icon_list +gtk_window_set_default_icon_name +gtk_window_set_default_size +gtk_window_set_deletable +gtk_window_set_destroy_with_parent +gtk_window_set_focus +gtk_window_set_focus_on_map +gtk_window_set_focus_visible +gtk_window_set_geometry_hints +gtk_window_set_gravity +gtk_window_set_has_resize_grip +gtk_window_set_has_user_ref_count +gtk_window_set_hide_titlebar_when_maximized +gtk_window_set_icon +gtk_window_set_icon_from_file +gtk_window_set_icon_list +gtk_window_set_icon_name +gtk_window_set_keep_above +gtk_window_set_keep_below +gtk_window_set_mnemonic_modifier +gtk_window_set_mnemonics_visible +gtk_window_set_modal +gtk_window_set_opacity +gtk_window_set_position +gtk_window_set_resizable +gtk_window_set_role +gtk_window_set_screen +gtk_window_set_skip_pager_hint +gtk_window_set_skip_taskbar_hint +gtk_window_set_startup_id +gtk_window_set_title +gtk_window_set_transient_for +gtk_window_set_type_hint +gtk_window_set_urgency_hint +gtk_window_set_wmclass +gtk_window_stick +gtk_window_unfullscreen +gtk_window_unmaximize +gtk_window_unstick +G_TLS_BACKEND_EXTENSION_POINT_NAME +g_tls_backend_get_certificate_type +g_tls_backend_get_client_connection_type +g_tls_backend_get_default +g_tls_backend_get_default_database +g_tls_backend_get_file_database_type +g_tls_backend_get_server_connection_type +GTlsBackendInterface +GTlsBackend +g_tls_backend_supports_tls +g_tls_certificate_get_issuer +g_tls_certificate_is_same +g_tls_certificate_list_new_from_file +g_tls_certificate_new_from_file +g_tls_certificate_new_from_files +g_tls_certificate_new_from_pem +GTlsCertificate +g_tls_certificate_verify +g_tls_client_connection_get_accepted_cas +g_tls_client_connection_get_server_identity +g_tls_client_connection_get_use_ssl3 +g_tls_client_connection_get_validation_flags +GTlsClientConnectionInterface +g_tls_client_connection_new +g_tls_client_connection_set_server_identity +g_tls_client_connection_set_use_ssl3 +g_tls_client_connection_set_validation_flags +GTlsClientConnection +g_tls_connection_emit_accept_certificate +g_tls_connection_get_certificate +g_tls_connection_get_database +g_tls_connection_get_interaction +g_tls_connection_get_peer_certificate +g_tls_connection_get_peer_certificate_errors +g_tls_connection_get_rehandshake_mode +g_tls_connection_get_require_close_notify +g_tls_connection_get_use_system_certdb +g_tls_connection_handshake +g_tls_connection_handshake_async +g_tls_connection_handshake_finish +g_tls_connection_set_certificate +g_tls_connection_set_database +g_tls_connection_set_interaction +g_tls_connection_set_rehandshake_mode +g_tls_connection_set_require_close_notify +g_tls_connection_set_use_system_certdb +GTlsConnection +g_tls_database_create_certificate_handle +g_tls_database_lookup_certificate_for_handle +g_tls_database_lookup_certificate_for_handle_async +g_tls_database_lookup_certificate_for_handle_finish +g_tls_database_lookup_certificate_issuer +g_tls_database_lookup_certificate_issuer_async +g_tls_database_lookup_certificate_issuer_finish +g_tls_database_lookup_certificates_issued_by +g_tls_database_lookup_certificates_issued_by_async +g_tls_database_lookup_certificates_issued_by_finish +G_TLS_DATABASE_PURPOSE_AUTHENTICATE_CLIENT +G_TLS_DATABASE_PURPOSE_AUTHENTICATE_SERVER +GTlsDatabase +g_tls_database_verify_chain +g_tls_database_verify_chain_async +g_tls_database_verify_chain_finish +G_TLS_ERROR +GTlsFileDatabaseInterface +g_tls_file_database_new +GTlsFileDatabase +g_tls_interaction_ask_password +g_tls_interaction_ask_password_async +g_tls_interaction_ask_password_finish +g_tls_interaction_invoke_ask_password +GTlsInteraction +GTlsPasswordClass +g_tls_password_get_description +g_tls_password_get_flags +g_tls_password_get_value +g_tls_password_get_warning +g_tls_password_new +g_tls_password_set_description +g_tls_password_set_flags +g_tls_password_set_value +g_tls_password_set_value_full +g_tls_password_set_warning +GTlsPassword +GTlsServerConnectionInterface +g_tls_server_connection_new +GTlsServerConnection +GToggleNotify +GTranslateFunc +GTrashStack +g_trash_stack_height +g_trash_stack_peek +g_trash_stack_pop +g_trash_stack_push +GTraverseFunc +GTree +g_tree_destroy +g_tree_foreach +g_tree_height +g_tree_insert +g_tree_lookup +g_tree_lookup_extended +g_tree_new +g_tree_new_full +g_tree_new_with_data +g_tree_nnodes +g_tree_ref +g_tree_remove +g_tree_replace +g_tree_search +g_tree_steal +g_tree_traverse +g_tree_unref +G_TRYLOCK +g_try_malloc +g_try_malloc0 +g_try_malloc0_n +g_try_malloc_n +g_try_new +g_try_new0 +g_try_realloc +g_try_realloc_n +g_try_renew +GTuples +g_tuples_destroy +g_tuples_index +GType +g_type_add_class_cache_func +g_type_add_class_private +g_type_add_interface_check +g_type_add_interface_dynamic +g_type_add_interface_static +G_TYPE_ARRAY +G_TYPE_BOOLEAN +G_TYPE_BOXED +G_TYPE_BYTE_ARRAY +G_TYPE_BYTES +G_TYPE_CHAR +G_TYPE_CHECK_CLASS_CAST +G_TYPE_CHECK_CLASS_TYPE +G_TYPE_CHECK_INSTANCE +G_TYPE_CHECK_INSTANCE_CAST +G_TYPE_CHECK_INSTANCE_TYPE +G_TYPE_CHECK_VALUE +G_TYPE_CHECK_VALUE_TYPE +g_type_children +GTypeClass +g_type_class_add_private +GTypeClassCacheFunc +G_TYPE_CLASS_GET_PRIVATE +g_type_class_peek +g_type_class_peek_parent +g_type_class_peek_static +g_type_class_ref +g_type_class_unref +g_type_class_unref_uncached +G_TYPE_CLOSURE +g_type_create_instance +G_TYPE_DATE +G_TYPE_DATE_TIME +G_TYPE_DBUS_ANNOTATION_INFO +G_TYPE_DBUS_ARG_INFO +G_TYPE_DBUS_INTERFACE_INFO +G_TYPE_DBUS_METHOD_INFO +G_TYPE_DBUS_NODE_INFO +G_TYPE_DBUS_PROPERTY_INFO +G_TYPE_DBUS_SIGNAL_INFO +g_type_default_interface_peek +g_type_default_interface_ref +g_type_default_interface_unref +g_type_depth +G_TYPE_DOUBLE +g_type_ensure +G_TYPE_ENUM +G_TYPE_ERROR +G_TYPE_FLAG_RESERVED_ID_BIT +G_TYPE_FLAGS +G_TYPE_FLOAT +g_type_free_instance +G_TYPE_FROM_CLASS +G_TYPE_FROM_INSTANCE +G_TYPE_FROM_INTERFACE +g_type_from_name +g_type_fundamental +G_TYPE_FUNDAMENTAL +GTypeFundamentalInfo +G_TYPE_FUNDAMENTAL_MAX +g_type_fundamental_next +g_type_get_plugin +g_type_get_qdata +g_type_get_type_registration_serial +G_TYPE_GSTRING +G_TYPE_GTYPE +G_TYPE_HASH_TABLE +G_TYPE_HAS_VALUE_TABLE +GTypeInfo +g_type_info_get_array_fixed_size +g_type_info_get_array_length +g_type_info_get_array_type +g_type_info_get_ffi_type +g_type_info_get_interface +g_type_info_get_param_type +g_type_info_get_tag +g_type_info_is_pointer +g_type_info_is_zero_terminated +g_type_init +G_TYPE_INITIALLY_UNOWNED +g_type_init_with_debug_flags +GTypeInstance +G_TYPE_INSTANCE_GET_CLASS +G_TYPE_INSTANCE_GET_INTERFACE +G_TYPE_INSTANCE_GET_PRIVATE +G_TYPE_INT +G_TYPE_INT64 +GTypeInterface +G_TYPE_INTERFACE +g_type_interface_add_prerequisite +GTypeInterfaceCheckFunc +g_type_interface_get_plugin +g_type_interface_peek +g_type_interface_peek_parent +g_type_interface_prerequisites +g_type_interfaces +G_TYPE_INVALID +G_TYPE_IO_CHANNEL +G_TYPE_IO_CONDITION +g_type_is_a +G_TYPE_IS_ABSTRACT +G_TYPE_IS_CLASSED +G_TYPE_IS_DEEP_DERIVABLE +G_TYPE_IS_DERIVABLE +G_TYPE_IS_DERIVED +G_TYPE_IS_ENUM +G_TYPE_IS_FLAGS +G_TYPE_IS_FUNDAMENTAL +G_TYPE_IS_INSTANTIATABLE +G_TYPE_IS_INTERFACE +G_TYPE_IS_OBJECT +G_TYPE_IS_PARAM +G_TYPE_IS_VALUE +G_TYPE_IS_VALUE_ABSTRACT +G_TYPE_IS_VALUE_TYPE +G_TYPE_KEY_FILE +g_typelib_check_sanity +G_TYPELIB_ERROR +g_typelib_free +g_typelib_get_dir_entry +g_typelib_get_namespace +g_typelib_get_string +g_typelib_new_from_const_memory +g_typelib_new_from_mapped_file +g_typelib_new_from_memory +g_typelib_symbol +g_typelib_validate +G_TYPE_LONG +G_TYPE_MAIN_CONTEXT +G_TYPE_MAIN_LOOP +G_TYPE_MAKE_FUNDAMENTAL +G_TYPE_MATCH_INFO +g_type_module_add_interface +GTypeModuleClass +g_type_module_register_enum +g_type_module_register_flags +g_type_module_register_type +g_type_module_set_name +GTypeModule +g_type_module_unuse +g_type_module_use +g_type_name +g_type_next_base +G_TYPE_NONE +G_TYPE_OBJECT +G_TYPE_PARAM +G_TYPE_PARAM_BOOLEAN +G_TYPE_PARAM_BOXED +G_TYPE_PARAM_CHAR +G_TYPE_PARAM_DOUBLE +G_TYPE_PARAM_ENUM +G_TYPE_PARAM_FLAGS +G_TYPE_PARAM_FLOAT +G_TYPE_PARAM_GTYPE +G_TYPE_PARAM_INT +G_TYPE_PARAM_INT64 +G_TYPE_PARAM_LONG +G_TYPE_PARAM_OBJECT +G_TYPE_PARAM_OVERRIDE +G_TYPE_PARAM_PARAM +G_TYPE_PARAM_POINTER +G_TYPE_PARAM_STRING +G_TYPE_PARAM_UCHAR +G_TYPE_PARAM_UINT +G_TYPE_PARAM_UINT64 +G_TYPE_PARAM_ULONG +G_TYPE_PARAM_UNICHAR +G_TYPE_PARAM_VALUE_ARRAY +G_TYPE_PARAM_VARIANT +g_type_parent +GTypePluginClass +g_type_plugin_complete_interface_info +GTypePluginCompleteInterfaceInfo +g_type_plugin_complete_type_info +GTypePluginCompleteTypeInfo +GTypePlugin +g_type_plugin_unuse +GTypePluginUnuse +g_type_plugin_use +GTypePluginUse +G_TYPE_POINTER +G_TYPE_POLLFD +G_TYPE_PTR_ARRAY +g_type_qname +g_type_query +GTypeQuery +G_TYPE_REGEX +g_type_register_dynamic +g_type_register_fundamental +g_type_register_static +g_type_register_static_simple +g_type_remove_class_cache_func +g_type_remove_interface_check +G_TYPE_RESERVED_BSE_FIRST +G_TYPE_RESERVED_BSE_LAST +G_TYPE_RESERVED_GLIB_FIRST +G_TYPE_RESERVED_GLIB_LAST +G_TYPE_RESERVED_USER_FIRST +g_type_set_qdata +G_TYPE_SETTINGS_SCHEMA +G_TYPE_SETTINGS_SCHEMA_SOURCE +G_TYPE_SOURCE +G_TYPE_STRING +G_TYPE_STRV +G_TYPE_TAG_IS_BASIC +g_type_tag_to_string +G_TYPE_THREAD +G_TYPE_TIME_ZONE +G_TYPE_UCHAR +G_TYPE_UINT +G_TYPE_UINT64 +G_TYPE_ULONG +G_TYPE_VALUE +G_TYPE_VALUE_ARRAY +GTypeValueTable +g_type_value_table_peek +G_TYPE_VARIANT +G_TYPE_VARIANT_BUILDER +G_TYPE_VARIANT_TYPE +g_ucs4_to_utf16 +g_ucs4_to_utf8 +gui_bg_color +gui_button_proc +gui_ctext_proc +gui_edit_proc +gui_fg_color +gui_font_baseline +gui_get_screen +gui_list_proc +gui_menu_draw_menu +gui_menu_draw_menu_item +gui_mg_color +gui_mouse_b +gui_mouse_focus +gui_mouse_x +gui_mouse_y +gui_mouse_z +GUINT16_FROM_BE +GUINT16_FROM_LE +GUINT16_SWAP_BE_PDP +GUINT16_SWAP_LE_BE +GUINT16_SWAP_LE_PDP +GUINT16_TO_BE +GUINT16_TO_LE +GUINT32_FROM_BE +GUINT32_FROM_LE +GUINT32_SWAP_BE_PDP +GUINT32_SWAP_LE_BE +GUINT32_SWAP_LE_PDP +GUINT32_TO_BE +GUINT32_TO_LE +GUINT64_FROM_BE +GUINT64_FROM_LE +GUINT64_SWAP_LE_BE +GUINT64_TO_BE +GUINT64_TO_LE +GUINT_FROM_BE +GUINT_FROM_LE +GUINT_TO_BE +GUINT_TO_LE +GUINT_TO_POINTER +gui_set_screen +gui_shadow_box_proc +gui_strlen +gui_text_list_proc +gui_textout_ex +GULONG_FROM_BE +GULONG_FROM_LE +GULONG_TO_BE +GULONG_TO_LE +G_UNAVAILABLE +g_unichar_break_type +g_unichar_combining_class +g_unichar_compose +g_unichar_decompose +g_unichar_digit_value +g_unichar_fully_decompose +g_unichar_get_mirror_char +g_unichar_get_script +g_unichar_isalnum +g_unichar_isalpha +g_unichar_iscntrl +g_unichar_isdefined +g_unichar_isdigit +g_unichar_isgraph +g_unichar_islower +g_unichar_ismark +g_unichar_isprint +g_unichar_ispunct +g_unichar_isspace +g_unichar_istitle +g_unichar_isupper +g_unichar_iswide +g_unichar_iswide_cjk +g_unichar_isxdigit +g_unichar_iszerowidth +G_UNICHAR_MAX_DECOMPOSITION_LENGTH +g_unichar_tolower +g_unichar_totitle +g_unichar_toupper +g_unichar_to_utf8 +g_unichar_type +g_unichar_validate +g_unichar_xdigit_value +g_unicode_canonical_decomposition +g_unicode_canonical_ordering +G_UNICODE_COMBINING_MARK +g_unicode_script_from_iso15924 +g_unicode_script_to_iso15924 +g_union_info_find_method +g_union_info_get_alignment +g_union_info_get_discriminator +g_union_info_get_discriminator_offset +g_union_info_get_discriminator_type +g_union_info_get_field +g_union_info_get_method +g_union_info_get_n_fields +g_union_info_get_n_methods +g_union_info_get_size +g_union_info_is_discriminated +g_unix_connection_receive_credentials +g_unix_connection_receive_credentials_async +g_unix_connection_receive_credentials_finish +g_unix_connection_receive_fd +g_unix_connection_send_credentials +g_unix_connection_send_credentials_async +g_unix_connection_send_credentials_finish +g_unix_connection_send_fd +GUnixConnection +GUnixCredentialsMessageClass +g_unix_credentials_message_get_credentials +g_unix_credentials_message_is_supported +g_unix_credentials_message_new +g_unix_credentials_message_new_with_credentials +GUnixCredentialsMessage +G_UNIX_ERROR +g_unix_fd_list_append +g_unix_fd_list_get +g_unix_fd_list_get_length +g_unix_fd_list_new +g_unix_fd_list_new_from_array +g_unix_fd_list_peek_fds +g_unix_fd_list_steal_fds +GUnixFDList +g_unix_fd_message_append_fd +g_unix_fd_message_get_fd_list +g_unix_fd_message_new +g_unix_fd_message_new_with_fd_list +g_unix_fd_message_steal_fds +GUnixFDMessage +g_unix_input_stream_get_close_fd +g_unix_input_stream_get_fd +g_unix_input_stream_new +g_unix_input_stream_set_close_fd +GUnixInputStream +g_unix_is_mount_path_system_internal +g_unix_mount_at +g_unix_mount_compare +GUnixMountEntry +g_unix_mount_free +g_unix_mount_get_device_path +g_unix_mount_get_fs_type +g_unix_mount_get_mount_path +g_unix_mount_guess_can_eject +g_unix_mount_guess_icon +g_unix_mount_guess_name +g_unix_mount_guess_should_display +g_unix_mount_guess_symbolic_icon +g_unix_mount_is_readonly +g_unix_mount_is_system_internal +g_unix_mount_monitor_new +g_unix_mount_monitor_set_rate_limit +GUnixMountMonitor +GUnixMountPoint +g_unix_mount_point_compare +g_unix_mount_point_free +g_unix_mount_point_get_device_path +g_unix_mount_point_get_fs_type +g_unix_mount_point_get_mount_path +g_unix_mount_point_get_options +g_unix_mount_point_guess_can_eject +g_unix_mount_point_guess_icon +g_unix_mount_point_guess_name +g_unix_mount_point_guess_symbolic_icon +g_unix_mount_point_is_loopback +g_unix_mount_point_is_readonly +g_unix_mount_point_is_user_mountable +g_unix_mount_points_changed_since +g_unix_mount_points_get +g_unix_mounts_changed_since +g_unix_mounts_get +g_unix_open_pipe +g_unix_output_stream_get_close_fd +g_unix_output_stream_get_fd +g_unix_output_stream_new +g_unix_output_stream_set_close_fd +GUnixOutputStream +g_unix_set_fd_nonblocking +g_unix_signal_add +g_unix_signal_add_full +g_unix_signal_source_new +g_unix_socket_address_abstract_names_supported +g_unix_socket_address_get_address_type +g_unix_socket_address_get_is_abstract +g_unix_socket_address_get_path +g_unix_socket_address_get_path_len +g_unix_socket_address_new +g_unix_socket_address_new_abstract +g_unix_socket_address_new_with_type +GUnixSocketAddress +G_UNLIKELY +g_unlink +G_UNLOCK +g_unsetenv +g_uri_escape_string +g_uri_list_extract_uris +g_uri_parse_scheme +G_URI_RESERVED_CHARS_ALLOWED_IN_PATH +G_URI_RESERVED_CHARS_ALLOWED_IN_PATH_ELEMENT +G_URI_RESERVED_CHARS_ALLOWED_IN_USERINFO +G_URI_RESERVED_CHARS_GENERIC_DELIMITERS +G_URI_RESERVED_CHARS_SUBCOMPONENT_DELIMITERS +g_uri_unescape_segment +g_uri_unescape_string +G_USEC_PER_SEC +g_usleep +g_utf16_to_ucs4 +g_utf16_to_utf8 +g_utf8_casefold +g_utf8_collate +g_utf8_collate_key +g_utf8_collate_key_for_filename +g_utf8_find_next_char +g_utf8_find_prev_char +g_utf8_get_char +g_utf8_get_char_validated +g_utf8_next_char +g_utf8_normalize +g_utf8_offset_to_pointer +g_utf8_pointer_to_offset +g_utf8_prev_char +g_utf8_strchr +g_utf8_strdown +g_utf8_strlen +g_utf8_strncpy +g_utf8_strrchr +g_utf8_strreverse +g_utf8_strup +g_utf8_substring +g_utf8_to_ucs4 +g_utf8_to_ucs4_fast +g_utf8_to_utf16 +g_utf8_validate +g_utime +GVaClosureMarshal +G_VA_COPY +GValue +GValueArray +g_value_array_append +g_value_array_copy +g_value_array_free +g_value_array_get_nth +g_value_array_insert +g_value_array_new +g_value_array_prepend +g_value_array_remove +g_value_array_sort +g_value_array_sort_with_data +G_VALUE_COLLECT +G_VALUE_COLLECT_FORMAT_MAX_LENGTH +G_VALUE_COLLECT_INIT +G_VALUE_COLLECT_SKIP +g_value_copy +g_value_dup_boxed +g_value_dup_object +g_value_dup_param +g_value_dup_string +g_value_dup_variant +g_value_fits_pointer +g_value_get_boolean +g_value_get_boxed +g_value_get_char +g_value_get_double +g_value_get_enum +g_value_get_flags +g_value_get_float +g_value_get_gtype +g_value_get_int +g_value_get_int64 +g_value_get_long +g_value_get_object +g_value_get_param +g_value_get_pointer +g_value_get_schar +g_value_get_string +g_value_get_uchar +g_value_get_uint +g_value_get_uint64 +g_value_get_ulong +g_value_get_variant +G_VALUE_HOLDS +G_VALUE_HOLDS_BOOLEAN +G_VALUE_HOLDS_BOXED +G_VALUE_HOLDS_CHAR +G_VALUE_HOLDS_DOUBLE +G_VALUE_HOLDS_ENUM +G_VALUE_HOLDS_FLAGS +G_VALUE_HOLDS_FLOAT +G_VALUE_HOLDS_GTYPE +G_VALUE_HOLDS_INT +G_VALUE_HOLDS_INT64 +G_VALUE_HOLDS_LONG +G_VALUE_HOLDS_OBJECT +G_VALUE_HOLDS_PARAM +G_VALUE_HOLDS_POINTER +G_VALUE_HOLDS_STRING +G_VALUE_HOLDS_UCHAR +G_VALUE_HOLDS_UINT +G_VALUE_HOLDS_UINT64 +G_VALUE_HOLDS_ULONG +G_VALUE_HOLDS_VARIANT +g_value_info_get_value +g_value_init +G_VALUE_INIT +G_VALUE_LCOPY +g_value_peek_pointer +g_value_register_transform_func +g_value_reset +g_value_set_boolean +g_value_set_boxed +g_value_set_boxed_take_ownership +g_value_set_char +g_value_set_double +g_value_set_enum +g_value_set_flags +g_value_set_float +g_value_set_gtype +g_value_set_instance +g_value_set_int +g_value_set_int64 +g_value_set_long +g_value_set_object +g_value_set_object_take_ownership +g_value_set_param +g_value_set_param_take_ownership +g_value_set_pointer +g_value_set_schar +g_value_set_static_boxed +g_value_set_static_string +g_value_set_string +g_value_set_string_take_ownership +g_value_set_uchar +g_value_set_uint +g_value_set_uint64 +g_value_set_ulong +g_value_set_variant +g_value_take_boxed +g_value_take_object +g_value_take_param +g_value_take_string +g_value_take_variant +g_value_transform +GValueTransform +G_VALUE_TYPE +g_value_type_compatible +G_VALUE_TYPE_NAME +g_value_type_transformable +g_value_unset +GVariant +GVariantBuilder +g_variant_builder_add +g_variant_builder_add_parsed +g_variant_builder_add_value +g_variant_builder_clear +g_variant_builder_close +g_variant_builder_end +g_variant_builder_init +g_variant_builder_new +g_variant_builder_open +g_variant_builder_ref +g_variant_builder_unref +g_variant_byteswap +g_variant_check_format_string +g_variant_classify +g_variant_compare +g_variant_dup_bytestring +g_variant_dup_bytestring_array +g_variant_dup_objv +g_variant_dup_string +g_variant_dup_strv +g_variant_equal +g_variant_get +g_variant_get_boolean +g_variant_get_byte +g_variant_get_bytestring +g_variant_get_bytestring_array +g_variant_get_child +g_variant_get_child_value +g_variant_get_data +g_variant_get_data_as_bytes +g_variant_get_double +g_variant_get_fixed_array +g_variant_get_handle +g_variant_get_int16 +g_variant_get_int32 +g_variant_get_int64 +g_variant_get_maybe +g_variant_get_normal_form +g_variant_get_objv +g_variant_get_size +g_variant_get_string +g_variant_get_strv +g_variant_get_type +g_variant_get_type_string +g_variant_get_uint16 +g_variant_get_uint32 +g_variant_get_uint64 +g_variant_get_va +g_variant_get_variant +g_variant_hash +g_variant_is_container +g_variant_is_floating +g_variant_is_normal_form +g_variant_is_object_path +g_variant_is_of_type +g_variant_is_signature +GVariantIter +g_variant_iter_copy +g_variant_iter_free +g_variant_iter_init +g_variant_iter_loop +g_variant_iter_n_children +g_variant_iter_new +g_variant_iter_next +g_variant_iter_next_value +g_variant_lookup +g_variant_lookup_value +g_variant_n_children +g_variant_new +g_variant_new_array +g_variant_new_boolean +g_variant_new_byte +g_variant_new_bytestring +g_variant_new_bytestring_array +g_variant_new_dict_entry +g_variant_new_double +g_variant_new_fixed_array +g_variant_new_from_bytes +g_variant_new_from_data +g_variant_new_handle +g_variant_new_int16 +g_variant_new_int32 +g_variant_new_int64 +g_variant_new_maybe +g_variant_new_object_path +g_variant_new_objv +g_variant_new_parsed +g_variant_new_parsed_va +g_variant_new_signature +g_variant_new_string +g_variant_new_strv +g_variant_new_tuple +g_variant_new_uint16 +g_variant_new_uint32 +g_variant_new_uint64 +g_variant_new_va +g_variant_new_variant +g_variant_parse +G_VARIANT_PARSE_ERROR +g_variant_print +g_variant_print_string +g_variant_ref +g_variant_ref_sink +g_variant_store +g_variant_take_ref +GVariantType +G_VARIANT_TYPE +G_VARIANT_TYPE_ANY +G_VARIANT_TYPE_ARRAY +G_VARIANT_TYPE_BASIC +G_VARIANT_TYPE_BOOLEAN +G_VARIANT_TYPE_BYTE +G_VARIANT_TYPE_BYTESTRING +G_VARIANT_TYPE_BYTESTRING_ARRAY +g_variant_type_copy +G_VARIANT_TYPE_DICT_ENTRY +G_VARIANT_TYPE_DICTIONARY +G_VARIANT_TYPE_DOUBLE +g_variant_type_dup_string +g_variant_type_element +g_variant_type_equal +g_variant_type_first +g_variant_type_free +g_variant_type_get_string_length +G_VARIANT_TYPE_HANDLE +g_variant_type_hash +G_VARIANT_TYPE_INT16 +G_VARIANT_TYPE_INT32 +G_VARIANT_TYPE_INT64 +g_variant_type_is_array +g_variant_type_is_basic +g_variant_type_is_container +g_variant_type_is_definite +g_variant_type_is_dict_entry +g_variant_type_is_maybe +g_variant_type_is_subtype_of +g_variant_type_is_tuple +g_variant_type_is_variant +g_variant_type_key +G_VARIANT_TYPE_MAYBE +g_variant_type_new +g_variant_type_new_array +g_variant_type_new_dict_entry +g_variant_type_new_maybe +g_variant_type_new_tuple +g_variant_type_next +g_variant_type_n_items +G_VARIANT_TYPE_OBJECT_PATH +G_VARIANT_TYPE_OBJECT_PATH_ARRAY +g_variant_type_peek_string +G_VARIANT_TYPE_SIGNATURE +G_VARIANT_TYPE_STRING +G_VARIANT_TYPE_STRING_ARRAY +g_variant_type_string_is_valid +g_variant_type_string_scan +G_VARIANT_TYPE_TUPLE +G_VARIANT_TYPE_UINT16 +G_VARIANT_TYPE_UINT32 +G_VARIANT_TYPE_UINT64 +G_VARIANT_TYPE_UNIT +g_variant_type_value +G_VARIANT_TYPE_VARDICT +G_VARIANT_TYPE_VARIANT +g_variant_unref +g_vasprintf +g_vfprintf +G_VFS_EXTENSION_POINT_NAME +g_vfs_get_default +g_vfs_get_file_for_path +g_vfs_get_file_for_uri +g_vfs_get_local +g_vfs_get_supported_uri_schemes +g_vfs_is_active +g_vfs_parse_name +GVfs +g_vfunc_info_get_flags +g_vfunc_info_get_invoker +g_vfunc_info_get_offset +g_vfunc_info_get_signal +GVoidFunc +g_volume_can_eject +g_volume_can_mount +g_volume_eject +g_volume_eject_finish +g_volume_eject_with_operation +g_volume_eject_with_operation_finish +g_volume_enumerate_identifiers +g_volume_get_activation_root +g_volume_get_drive +g_volume_get_icon +g_volume_get_identifier +g_volume_get_mount +g_volume_get_name +g_volume_get_sort_key +g_volume_get_symbolic_icon +g_volume_get_uuid +G_VOLUME_IDENTIFIER_KIND_CLASS +G_VOLUME_IDENTIFIER_KIND_HAL_UDI +G_VOLUME_IDENTIFIER_KIND_LABEL +G_VOLUME_IDENTIFIER_KIND_NFS_MOUNT +G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE +G_VOLUME_IDENTIFIER_KIND_UUID +GVolumeIface +g_volume_monitor_adopt_orphan_mount +G_VOLUME_MONITOR_EXTENSION_POINT_NAME +g_volume_monitor_get +g_volume_monitor_get_connected_drives +g_volume_monitor_get_mount_for_uuid +g_volume_monitor_get_mounts +g_volume_monitor_get_volume_for_uuid +g_volume_monitor_get_volumes +GVolumeMonitor +g_volume_mount +g_volume_mount_finish +g_volume_should_automount +GVolume +g_vprintf +g_vsnprintf +g_vsprintf +g_warn_if_fail +g_warn_if_reached +g_warning +GWeakNotify +GWeakRef +g_weak_ref_clear +g_weak_ref_get +g_weak_ref_init +g_weak_ref_set +G_WIN32_DLLMAIN_FOR_DLL_NAME +g_win32_error_message +g_win32_getlocale +g_win32_get_package_installation_directory +g_win32_get_package_installation_directory_of_module +g_win32_get_package_installation_subdirectory +g_win32_get_windows_version +G_WIN32_HAVE_WIDECHAR_API +GWin32InputStream +g_win32_input_stream_get_close_handle +g_win32_input_stream_get_handle +g_win32_input_stream_new +g_win32_input_stream_set_close_handle +G_WIN32_IS_NT_BASED +g_win32_locale_filename_from_utf8 +GWin32OutputStream +g_win32_output_stream_get_close_handle +g_win32_output_stream_get_handle +g_win32_output_stream_new +g_win32_output_stream_set_close_handle +g_zlib_compressor_get_file_info +g_zlib_compressor_new +g_zlib_compressor_set_file_info +GZlibCompressor +g_zlib_decompressor_get_file_info +g_zlib_decompressor_new +GZlibDecompressor +halfdelay_sp +has_colors +has_colors_sp +has_ic +has_ic_sp +has_il +has_il_sp +has_key +has_key_sp +has_mouse +has_mouse_sp +hcreate_r +hdestroy_r +h_errno +hide_panel +hline_set +hook_config_section +hsearch_r +hsv_to_rgb +HUGE_VAL +HUGE_VALF +HUGE_VALL +iconv_close +iconv_open +identity_matrix +identity_matrix_f +identity_quat +inet_addr +inet_aton +inet_lnaof +inet_makeaddr +inet_netof +inet_network +inet_ntoa +inet_ntop +inet_pton +init_color +init_color_sp +init_dialog +initialise_joystick +init_menu +init_pair +init_pair_sp +initstate_r +ins_nwstr +install_allegro +install_int +install_int_ex +install_joystick +install_keyboard +install_keyboard_hooks +install_mouse +install_param_int +install_param_int_ex +install_sound +install_sound_input +install_timer +ins_wch +ins_wstr +intrflush_sp +in_wch +in_wchnstr +in_wchstr +iruserok_af +is_cleared +is_color_font +is_compatible_font +isendwin_sp +is_idcok +is_idlok +is_immedok +is_inside_bitmap +is_keypad +is_leaveok +is_linear_bitmap +is_linetouched +is_memory_bitmap +is_mono_font +is_nodelay +is_notimeout +is_pad +is_planar_bitmap +is_relative_filename +is_same_bitmap +is_screen_bitmap +is_scrollok +is_sub_bitmap +is_subwin +is_syncok +is_system_bitmap +is_term_resized +is_term_resized_sp +is_trans_font +is_video_bitmap +is_windowed_mode +is_wintouched +item_count +item_description +item_index +item_init +item_name +item_opts +item_opts_off +item_opts_on +item_term +item_userptr +item_value +item_visible +JOYSTICK_AXIS_INFO +JOYSTICK_BUTTON_INFO +JOYSTICK_INFO +JOYSTICK_STICK_INFO +jrand48_r +keyboard_callback +keyboard_lowlevel_callback +keyboard_needs_poll +keyboard_ucallback +keybound_sp +key_decryptsession +key_defined +key_defined_sp +key_encryptsession +key_gendes +key_led_flag +key_name +keyname_sp +keyok_sp +key_secretkey_is_set +key_setsecret +key_shifts +killchar_sp +lcong48_r +legacy_coding +lgammaf_r +lgammal_r +lgamma_r +libtasn1_perror +libtasn1_strerror +link_field +link_fieldtype +lio_listio +list_config_entries +list_config_sections +LIST_ENTRY +LIST_HEAD +LIST_INIT +LIST_INSERT_AFTER +LIST_INSERT_HEAD +LIST_REMOVE +load_bios_font +load_bitmap +load_bitmap_font +load_bmp +load_bmp_pf +load_datafile +load_datafile_callback +load_datafile_object +load_datafile_object_indexed +load_dat_font +load_font +load_grx_font +load_grx_or_bios_font +load_ibk +load_joystick_data +load_lbm +load_midi +load_midi_patches +load_pcx +load_pcx_pf +load_sample +load_tga +load_tga_pf +load_txt_font +load_voc +load_voc_pf +load_wav +load_wav_pf +localtime_r +lock_bitmap +LOCK_FUNCTION +lock_midi +lock_sample +LOCK_VARIABLE +login_tty +lrand48_r +LZSS_PACK_DATA +lzss_read +LZSS_UNPACK_DATA +lzss_write +make_absolute_filename +makeacol_depth +makecol15_dither +makecol16_dither +makecol_depth +make_relative_filename +make_trans_font +MAKE_VERSION +malloc_get_state +__malloc_hook +malloc_hook +malloc_info +malloc_set_state +malloc_stats +malloc_trim +malloc_usable_size +MASK_COLOR_15 +MASK_COLOR_16 +MASK_COLOR_24 +MASK_COLOR_32 +MASK_COLOR_8 +masked_blit +masked_stretch_blit +MATRIX_f +matrix_mul +matrix_mul_f +matrix_to_quat +MB_CUR_MAX +MB_LEN_MAX +mcheck_check_all +mcheck_pedantic +mcprint_sp +menu_attributes +menu_back +menu_cursor +menu_driver +menu_fore +menu_format +menu_grey +menu_hook +menu_init +menu_items +menu_mark +menu_new +menu_opts +menu_opts_off +menu_opts_on +menu_pad +menu_pattern +MENU_PLAYER +menu_post +menu_request_by_name +menu_request_name +menu_requestname +menu_spacing +menu_sub +menu_term +menu_userptr +menu_win +merge_fonts +midi_loop_end +midi_loop_start +midi_meta_callback +midi_msg_callback +midi_out +midi_pause +midi_pos +midi_recorder +midi_resume +midi_seek +midi_sysex_callback +midi_time +mitem_current +mitem_name +mitem_new +mitem_opts +mitem_userptr +mitem_value +mitem_visible +mouse_b +mouse_callback +mouseinterval_sp +mousemask_sp +mouse_needs_poll +mouse_on_screen +mouse_pos +mouse_sprite +mouse_trafo +mouse_w +mouse_x +mouse_x_focus +mouse_y +mouse_y_focus +mouse_z +move_field +move_panel +mpath_persistent_reserve_in +mpath_persistent_reserve_out +mq_close +mq_getattr +mq_notify +mq_open +mq_receive +mq_send +mq_setattr +mq_timedreceive +mq_timedsend +mq_unlink +mrand48_r +mvadd_wch +mvadd_wchnstr +mvadd_wchstr +mvcur_sp +mvgetn_wstr +mvget_wch +mvget_wstr +mvhline_set +mvins_nwstr +mvins_wch +mvins_wstr +mvin_wch +mvin_wchnstr +mvin_wchstr +mvvline_set +mvwadd_wch +mvwadd_wchnstr +mvwadd_wchstr +mvwgetn_wstr +mvwget_wch +mvwget_wstr +mvwhline_set +mvwins_nwstr +mvwins_wch +mvwins_wstr +mvwin_wch +mvwin_wchnstr +mvwin_wchstr +mvwvline_set +N_ +napms_sp +NC_ +_nc_freeall +_nc_free_and_exit +_nc_tracebits +NDBM_File +need_uconvert +new_field +new_fieldtype +new_form +new_form_sp +new_item +new_menu +new_menu_sp +newpad_sp +new_page +new_panel +new_prescr +newterm_sp +newwin_sp +next_fli_frame +nl_langinfo +nl_sp +nocbreak_sp +noecho_sp +nofilter_sp +nonl_sp +noqiflush_sp +noraw_sp +normalize_vector +normalize_vector_f +nrand48_r +num_joysticks +object_message +ODBM_File +offer_focus +on_exit +open_fli +open_memory_fli +open_memstream +open_wmemstream +os_multitasking +os_revision +os_type +os_version +override_config_data +override_config_file +pack_fclose +pack_fclose_chunk +pack_feof +pack_ferror +pack_fgets +packfile_password +PACKFILE_VTABLE +pack_fopen +pack_fopen_chunk +pack_fopen_vtable +pack_fputs +pack_fread +pack_fseek +pack_fwrite +pack_getc +pack_igetl +pack_igetw +pack_iputl +pack_iputw +pack_mgetl +pack_mgetw +pack_mputl +pack_mputw +pack_putc +pack_ungetc +pair_content +pair_content_sp +PAIR_NUMBER +palette_color +PAL_SIZE +panel_above +panel_below +panel_hidden +panel_userptr +panel_window +pecho_wchar +persp_project +persp_project_f +pivot_scaled_sprite +pivot_scaled_sprite_lit +pivot_scaled_sprite_trans +pivot_scaled_sprite_v_flip +pivot_scaled_sprite_v_flip_lit +pivot_scaled_sprite_v_flip_trans +pivot_sprite +pivot_sprite_lit +pivot_sprite_trans +pivot_sprite_v_flip +pivot_sprite_v_flip_lit +pivot_sprite_v_flip_trans +play_audio_stream +play_fli +play_looped_midi +play_memory_fli +play_midi +play_sample +pmap_getmaps +pmap_getport +pmap_rmtcall +pmap_set +pmap_unset +poll_joystick +poll_keyboard +poll_mouse +poll_scroll +polygon3d_f +polygon_z_normal +polygon_z_normal_f +POLYTYPE_ATEX +POLYTYPE_ATEX_LIT +POLYTYPE_ATEX_MASK +POLYTYPE_ATEX_MASK_LIT +POLYTYPE_ATEX_MASK_TRANS +POLYTYPE_ATEX_TRANS +POLYTYPE_FLAT +POLYTYPE_GCOL +POLYTYPE_GRGB +POLYTYPE_PTEX +POLYTYPE_PTEX_LIT +POLYTYPE_PTEX_MASK +POLYTYPE_PTEX_MASK_LIT +POLYTYPE_PTEX_MASK_TRANS +POLYTYPE_PTEX_TRANS +pop_config_state +popup_dialog +pos_form_cursor +position_dialog +position_mouse +position_mouse_w +position_mouse_z +posix_fallocate +posix_memalign +posix_openpt +pos_menu_cursor +post_form +post_menu +program_invocation_name +program_invocation_short_name +pthread_attr_destroy +pthread_attr_getaffinity_np +pthread_attr_getdetachstate +pthread_attr_getguardsize +pthread_attr_getinheritsched +pthread_attr_getschedparam +pthread_attr_getschedpolicy +pthread_attr_getscope +pthread_attr_getstack +pthread_attr_getstackaddr +pthread_attr_getstacksize +pthread_attr_init +pthread_attr_setaffinity_np +pthread_attr_setdetachstate +pthread_attr_setguardsize +pthread_attr_setinheritsched +pthread_attr_setschedparam +pthread_attr_setschedpolicy +pthread_attr_setscope +pthread_attr_setstack +pthread_attr_setstackaddr +pthread_attr_setstacksize +pthread_cancel +pthread_cleanup_pop +pthread_cleanup_pop_restore_np +pthread_cleanup_push +pthread_cleanup_push_defer_np +pthread_create +pthread_detach +pthread_equal +pthread_exit +pthread_getaffinity_np +pthread_getattr_np +pthread_getconcurrency +pthread_getcpuclockid +pthread_getschedparam +pthread_join +pthread_kill +pthread_kill_other_threads_np +pthread_self +pthread_setaffinity_np +pthread_setcancelstate +pthread_setcanceltype +pthread_setconcurrency +pthread_setschedparam +pthread_setschedprio +pthread_sigmask +pthread_sigqueue +pthread_testcancel +pthread_timedjoin_np +pthread_tryjoin_np +pthread_yield +ptsname_r +push_config_state +put_backslash +putchar_unlocked +putc_unlocked +_putpixel +_putpixel15 +_putpixel16 +_putpixel24 +_putpixel32 +putp_sp +putwchar_unlocked +putwc_unlocked +Q_ +qecvt_r +qfcvt_r +qiflush_sp +qnx_get_window +qscale_matrix +qscale_matrix_f +qsort_r +qtranslate_matrix +qtranslate_matrix_f +quad3d_f +quat_interpolate +quat_mul +quat_slerp +quat_to_matrix +radtofix_r +random_r +rand_r +raw_sp +rcmd_af +readdir_r +read_sound_input +reallocate_voice +re_comp +re_exec +register_assert_handler +register_bitmap_file_type +register_datafile_object +register_font_file_type +register_sample_file_type +register_trace_handler +register_uformat +release_bitmap +release_screen +release_voice +reload_config_texts +remove_display_switch_callback +remove_int +remove_joystick +remove_keyboard +remove_mouse +remove_param_int +remove_sound +remove_sound_input +remove_timer +render_scene +replace_extension +replace_filename +replace_panel +request_refresh_rate +request_scroll +request_video_bitmap +reserve_voices +reset_fli_variables +reset_prog_mode +reset_prog_mode_sp +reset_shell_mode +reset_shell_mode_sp +resetty_sp +res_init +resize_term +resize_term_sp +resizeterm_sp +res_mkquery +res_query +res_querydomain +res_search +res_send +restartterm_sp +rest_callback +retrace_count +rexec_af +rgb_map +RGB_MAP +rgb_to_hsv +ripoffline_sp +RLE_SPRITE +rotate_scaled_sprite +rotate_scaled_sprite_lit +rotate_scaled_sprite_trans +rotate_scaled_sprite_v_flip +rotate_scaled_sprite_v_flip_lit +rotate_scaled_sprite_v_flip_trans +rotate_sprite +rotate_sprite_lit +rotate_sprite_trans +rotate_sprite_v_flip +rotate_sprite_v_flip_lit +rotate_sprite_v_flip_trans +rresvport_af +ruserok_af +save_bitmap +save_bmp +save_bmp_pf +save_joystick_data +save_pcx +save_pcx_pf +save_sample +save_tga +save_tga_pf +savetty_sp +scale_form +scale_menu +scancode_to_ascii +scancode_to_name +scare_mouse +scare_mouse_area +scene_gap +scene_polygon3d +scene_polygon3d_f +sched_getcpu +scr_dump +SCREEN_H +SCREEN_W +scr_init +scr_init_sp +scroll_screen +scr_restore +scr_restore_sp +scr_set +scr_set_sp +SDBM_File +secure_getenv +seed48_r +select_mouse_cursor +select_palette +sem_close +sem_destroy +sem_getvalue +sem_init +sem_open +sem_post +sem_timedwait +sem_trywait +sem_unlink +sem_wait +set_add_blender +set_allegro_resource_path +set_alpha_blender +set_blender_mode +set_blender_mode_ex +set_burn_blender +set_clip_rect +set_clip_state +set_close_button_callback +_set_color +set_color +set_color_blender +set_color_conversion +set_color_depth +set_config_data +set_config_file +set_config_float +set_config_hex +set_config_id +set_config_int +set_config_string +set_current_field +set_current_item +set_curterm +set_curterm_sp +set_dialog_color +set_difference_blender +set_display_switch_callback +set_display_switch_mode +set_dissolve_blender +set_dodge_blender +set_escdelay +set_escdelay_sp +set_field_back +set_field_buffer +set_field_fore +set_field_init +set_field_just +set_field_opts +set_field_pad +set_field_status +set_field_term +set_field_type +set_fieldtype_arg +set_fieldtype_choice +set_field_userptr +set_filename_encoding +set_form_fields +set_form_init +set_form_opts +set_form_page +set_form_sub +set_form_term +set_form_userptr +set_form_win +__setfpucw +set_gfx_mode +set_hardware_volume +set_hue_blender +set_invert_blender +set_item_init +set_item_opts +set_item_term +set_item_userptr +set_item_value +set_keyboard_rate +setkey_r +set_leds +set_luminance_blender +set_max_field +set_menu_back +set_menu_fore +set_menu_format +set_menu_grey +set_menu_init +set_menu_items +set_menu_mark +set_menu_opts +set_menu_pad +set_menu_pattern +set_menu_spacing +set_menu_sub +set_menu_term +set_menu_userptr +set_menu_win +set_mixer_quality +set_mouse_cursor_bitmap +set_mouse_range +set_mouse_speed +set_mouse_sprite +set_mouse_sprite_focus +set_multiply_blender +set_new_page +set_palette +set_palette_range +set_panel_userptr +set_projection_viewport +set_saturation_blender +set_screen_blender +set_sound_input_source +setstate_r +set_tabsize +set_tabsize_sp +set_term +set_top_row +set_trans_blender +set_ucodepage +set_uformat +set_volume +set_volume_per_voice +set_window_title +set_write_alpha_blender +set_zbuffer +sgetspent_r +shm_open +shm_unlink +show_mouse +show_os_cursor +show_panel +show_video_bitmap +shutdown_dialog +shutdown_menu +simulate_keypress +simulate_ukeypress +slk_attr +slk_attr_off +slk_attroff +slk_attroff_sp +slk_attr_on +slk_attron +slk_attron_sp +slk_attr_set +slk_attrset +slk_attr_set_sp +slk_attrset_sp +slk_attr_sp +slk_clear +slk_clear_sp +slk_color +slk_color_sp +slk_init +slk_init_sp +slk_label +slk_label_sp +slk_noutrefresh +slk_noutrefresh_sp +slk_refresh +slk_refresh_sp +slk_restore +slk_restore_sp +slk_set +slk_set_sp +slk_touch +slk_touch_sp +slk_wset +solid_mode +srand48_r +srandom_r +start_color +start_color_sp +start_sound_input +stdio_ext +stop_audio_stream +stop_midi +stop_sample +stop_sound_input +strerror_r +stretch_blit +stretch_sprite +strtok_r +svc_destroy +svcerr_auth +svcerr_decode +svcerr_noproc +svcerr_noprog +svcerr_progvers +svcerr_systemerr +svcerr_weakauth +svcfd_create +svc_freeargs +svc_getargs +svc_getcaller +svc_getreq +svc_getreqset +svcraw_create +svc_register +svc_run +svc_sendreply +svctcp_create +svcudp_bufcreate +svcudp_create +svc_unregister +sys_errlist +sys_nerr +sysv_signal +TAILQ_ENTRY +TAILQ_HEAD +TAILQ_INIT +TAILQ_INSERT_AFTER +TAILQ_INSERT_HEAD +TAILQ_INSERT_TAIL +TAILQ_REMOVE +Tcl_Access +Tcl_AddErrorInfo +Tcl_AddObjErrorInfo +Tcl_AlertNotifier +Tcl_Alloc +Tcl_AllocStatBuf +Tcl_AllowExceptions +Tcl_AppendAllObjTypes +Tcl_AppendElement +Tcl_AppendExportList +Tcl_AppendFormatToObj +Tcl_AppendLimitedToObj +Tcl_AppendObjToErrorInfo +Tcl_AppendObjToObj +Tcl_AppendPrintfToObj +Tcl_AppendResult +Tcl_AppendResultVA +Tcl_AppendStringsToObj +Tcl_AppendStringsToObjVA +Tcl_AppendToObj +Tcl_AppendUnicodeToObj +Tcl_AppInit +Tcl_AsyncCreate +Tcl_AsyncDelete +Tcl_AsyncInvoke +Tcl_AsyncMark +Tcl_AsyncReady +Tcl_AttemptAlloc +Tcl_AttemptRealloc +Tcl_AttemptSetObjLength +Tcl_BackgroundError +Tcl_Backslash +Tcl_BadChannelOption +Tcl_CallWhenDeleted +Tcl_CancelIdleCall +Tcl_ChannelBlockModeProc +Tcl_ChannelBuffered +Tcl_ChannelClose2Proc +Tcl_ChannelCloseProc +Tcl_ChannelFlushProc +Tcl_ChannelGetHandleProc +Tcl_ChannelGetOptionProc +Tcl_ChannelHandlerProc +Tcl_ChannelInputProc +Tcl_ChannelName +Tcl_ChannelOutputProc +Tcl_ChannelSeekProc +Tcl_ChannelSetOptionProc +Tcl_ChannelThreadActionProc +Tcl_ChannelTruncateProc +Tcl_ChannelVersion +Tcl_ChannelWatchProc +Tcl_ChannelWideSeekProc +Tcl_Chdir +Tcl_ClearChannelHandlers +Tcl_Close +Tcl_CommandComplete +Tcl_CommandTraceInfo +Tcl_Concat +Tcl_ConcatObj +Tcl_ConditionFinalize +Tcl_ConditionNotify +Tcl_ConditionWait +Tcl_ConvertCountedElement +Tcl_ConvertElement +Tcl_ConvertToType +Tcl_CreateAlias +Tcl_CreateAliasObj +Tcl_CreateChannel +Tcl_CreateChannelHandler +Tcl_CreateCloseHandler +Tcl_CreateCommand +Tcl_CreateEncoding +Tcl_CreateEnsemble +Tcl_CreateEventSource +Tcl_CreateExitHandler +Tcl_CreateFileHandler +Tcl_CreateHashEntry +Tcl_CreateInterp +Tcl_CreateMathFunc +Tcl_CreateNamespace +Tcl_CreateObjCommand +Tcl_CreateObjTrace +Tcl_CreateSlave +Tcl_CreateThread +Tcl_CreateThreadExitHandler +Tcl_CreateTimerHandler +Tcl_CreateTrace +Tcl_CutChannel +Tcl_DecrRefCount +Tcl_DeleteAssocData +Tcl_DeleteChannelHandler +Tcl_DeleteCloseHandler +Tcl_DeleteCommand +Tcl_DeleteCommandFromToken +Tcl_DeleteEvents +Tcl_DeleteEventSource +Tcl_DeleteExitHandler +Tcl_DeleteFileHandler +Tcl_DeleteHashEntry +Tcl_DeleteHashTable +Tcl_DeleteInterp +Tcl_DeleteNamespace +Tcl_DeleteThreadExitHandler +Tcl_DeleteTimerHandler +Tcl_DeleteTrace +Tcl_DetachChannel +Tcl_DetachPids +Tcl_DictObjDone +Tcl_DictObjFirst +Tcl_DictObjGet +Tcl_DictObjNext +Tcl_DictObjPut +Tcl_DictObjPutKeyList +Tcl_DictObjRemove +Tcl_DictObjRemoveKeyList +Tcl_DictObjSize +Tcl_DiscardInterpState +Tcl_DiscardResult +Tcl_DontCallWhenDeleted +Tcl_DoOneEvent +Tcl_DoWhenIdle +Tcl_DStringAppend +Tcl_DStringAppendElement +Tcl_DStringEndSublist +Tcl_DStringFree +Tcl_DStringGetResult +Tcl_DStringInit +Tcl_DStringLength +Tcl_DStringResult +Tcl_DStringSetLength +Tcl_DStringStartSublist +Tcl_DStringTrunc +Tcl_DStringValue +Tcl_DumpActiveMemory +Tcl_DuplicateObj +Tcl_Eof +Tcl_ErrnoId +Tcl_ErrnoMsg +Tcl_Eval +Tcl_EvalEx +Tcl_EvalFile +Tcl_EvalObjEx +Tcl_EvalObjv +Tcl_EvalTokens +Tcl_EvalTokensStandard +Tcl_EventuallyFree +Tcl_Exit +Tcl_ExitThread +Tcl_Export +Tcl_ExposeCommand +Tcl_ExprBoolean +Tcl_ExprBooleanObj +Tcl_ExprDouble +Tcl_ExprDoubleObj +Tcl_ExprLong +Tcl_ExprLongObj +Tcl_ExprObj +Tcl_ExprString +Tcl_ExternalToUtf +Tcl_ExternalToUtfDString +Tcl_Finalize +Tcl_FinalizeNotifier +Tcl_FinalizeThread +Tcl_FindCommand +Tcl_FindEnsemble +Tcl_FindExecutable +Tcl_FindHashEntry +Tcl_FindNamespace +Tcl_FirstHashEntry +Tcl_Flush +Tcl_ForgetImport +Tcl_Format +Tcl_Free +Tcl_FreeEncoding +Tcl_FreeParse +Tcl_FreeResult +Tcl_FSAccess +Tcl_FSChdir +Tcl_FSConvertToPathType +Tcl_FSCopyDirectory +Tcl_FSCopyFile +Tcl_FSCreateDirectory +Tcl_FSData +Tcl_FSDeleteFile +Tcl_FSEqualPaths +Tcl_FSEvalFile +Tcl_FSEvalFileEx +Tcl_FSFileAttrsGet +Tcl_FSFileAttrsSet +Tcl_FSFileAttrStrings +Tcl_FSFileSystemInfo +Tcl_FSGetCwd +Tcl_FSGetFileSystemForPath +Tcl_FSGetInternalRep +Tcl_FSGetNativePath +Tcl_FSGetNormalizedPath +Tcl_FSGetPathType +Tcl_FSGetTranslatedPath +Tcl_FSGetTranslatedStringPath +Tcl_FSJoinPath +Tcl_FSJoinToPath +Tcl_FSLink +Tcl_FSListVolumes +Tcl_FSLoadFile +Tcl_FSLstat +Tcl_FSMatchInDirectory +Tcl_FSMountsChanged +Tcl_FSNewNativePath +Tcl_FSOpenFileChannel +Tcl_FSPathSeparator +Tcl_FSRegister +Tcl_FSRemoveDirectory +Tcl_FSRenameFile +Tcl_FSSplitPath +Tcl_FSStat +Tcl_FSUnregister +Tcl_FSUtime +Tcl_GetAlias +Tcl_GetAliasObj +Tcl_GetAssocData +Tcl_GetBignumFromObj +Tcl_GetBoolean +Tcl_GetBooleanFromObj +Tcl_GetByteArrayFromObj +Tcl_GetChannel +Tcl_GetChannelBufferSize +Tcl_GetChannelError +Tcl_GetChannelErrorInterp +Tcl_GetChannelHandle +Tcl_GetChannelInstanceData +Tcl_GetChannelMode +Tcl_GetChannelName +Tcl_GetChannelNames +Tcl_GetChannelNamesEx +Tcl_GetChannelOption +Tcl_GetChannelThread +Tcl_GetChannelType +Tcl_GetCharLength +Tcl_GetCommandFromObj +Tcl_GetCommandFullName +Tcl_GetCommandInfo +Tcl_GetCommandInfoFromToken +Tcl_GetCommandName +Tcl_GetCurrentNamespace +Tcl_GetCurrentThread +Tcl_GetCwd +Tcl_GetDefaultEncodingDir +Tcl_GetDouble +Tcl_GetDoubleFromObj +Tcl_GetEncoding +Tcl_GetEncodingFromObj +Tcl_GetEncodingName +Tcl_GetEncodingNameFromEnvironment +Tcl_GetEncodingNames +Tcl_GetEncodingSearchPath +Tcl_GetEnsembleFlags +Tcl_GetEnsembleMappingDict +Tcl_GetEnsembleNamespace +Tcl_GetEnsembleSubcommandList +Tcl_GetEnsembleUnknownHandler +Tcl_GetErrno +Tcl_GetGlobalNamespace +Tcl_GetHashKey +Tcl_GetHashValue +Tcl_GetHostName +Tcl_GetIndexFromObj +Tcl_GetIndexFromObjStruct +Tcl_GetInt +Tcl_GetInterpPath +Tcl_GetIntFromObj +Tcl_GetLongFromObj +Tcl_GetMaster +Tcl_GetMathFuncInfo +Tcl_GetNameOfExecutable +Tcl_GetNamespaceUnknownHandler +Tcl_GetObjResult +Tcl_GetObjType +Tcl_GetOpenFile +Tcl_GetPathType +Tcl_GetRange +Tcl_GetRegExpFromObj +Tcl_GetReturnOptions +Tcl_Gets +Tcl_GetServiceMode +Tcl_GetSlave +Tcl_GetsObj +Tcl_GetStackedChannel +Tcl_GetStdChannel +Tcl_GetString +Tcl_GetStringFromObj +Tcl_GetStringResult +Tcl_GetThreadData +Tcl_GetTime +Tcl_GetTopChannel +Tcl_GetUniChar +Tcl_GetUnicode +Tcl_GetUnicodeFromObj +Tcl_GetVar +Tcl_GetVar2 +Tcl_GetVar2Ex +Tcl_GetVersion +Tcl_GetWideIntFromObj +Tcl_GlobalEval +Tcl_GlobalEvalObj +Tcl_HashStats +Tcl_HideCommand +Tcl_Import +Tcl_IncrRefCount +Tcl_Init +Tcl_InitCustomHashTable +Tcl_InitHashTable +Tcl_InitMemory +Tcl_InitNotifier +Tcl_InitObjHashTable +Tcl_InitStubs +Tcl_InputBlocked +Tcl_InputBuffered +Tcl_Interp +Tcl_InterpDeleted +Tcl_InvalidateStringRep +Tcl_IsChannelExisting +Tcl_IsChannelRegistered +Tcl_IsChannelShared +Tcl_IsEnsemble +Tcl_IsSafe +Tcl_IsShared +Tcl_IsStandardChannel +Tcl_JoinPath +Tcl_JoinThread +Tcl_LimitAddHandler +Tcl_LimitCheck +Tcl_LimitExceeded +Tcl_LimitGetCommands +Tcl_LimitGetGranularity +Tcl_LimitGetTime +Tcl_LimitReady +Tcl_LimitRemoveHandler +Tcl_LimitSetCommands +Tcl_LimitSetGranularity +Tcl_LimitSetTime +Tcl_LimitTypeEnabled +Tcl_LimitTypeExceeded +Tcl_LimitTypeReset +Tcl_LimitTypeSet +Tcl_LinkVar +Tcl_ListMathFuncs +Tcl_ListObjAppendElement +Tcl_ListObjAppendList +Tcl_ListObjGetElements +Tcl_ListObjIndex +Tcl_ListObjLength +Tcl_ListObjReplace +Tcl_LogCommandInfo +Tcl_Main +Tcl_MakeFileChannel +Tcl_MakeSafe +Tcl_MakeTcpClientChannel +TCL_MEM_DEBUG +Tcl_Merge +Tcl_MutexFinalize +Tcl_MutexLock +Tcl_MutexUnlock +Tcl_NewBignumObj +Tcl_NewBooleanObj +Tcl_NewByteArrayObj +Tcl_NewDictObj +Tcl_NewDoubleObj +Tcl_NewIntObj +Tcl_NewListObj +Tcl_NewLongObj +Tcl_NewObj +Tcl_NewStringObj +Tcl_NewUnicodeObj +Tcl_NewWideIntObj +Tcl_NextHashEntry +Tcl_NotifyChannel +Tcl_NumUtfChars +Tcl_ObjGetVar2 +Tcl_ObjPrintf +Tcl_ObjSetVar2 +Tcl_OpenCommandChannel +Tcl_OpenFileChannel +Tcl_OpenTcpClient +Tcl_OpenTcpServer +Tcl_OutputBuffered +Tcl_Panic +Tcl_PanicVA +Tcl_ParseBraces +Tcl_ParseCommand +Tcl_ParseExpr +Tcl_ParseQuotedString +Tcl_ParseVar +Tcl_ParseVarName +Tcl_PkgPresent +Tcl_PkgPresentEx +Tcl_PkgProvide +Tcl_PkgProvideEx +Tcl_PkgRequire +Tcl_PkgRequireEx +Tcl_PkgRequireProc +Tcl_PosixError +Tcl_Preserve +Tcl_PrintDouble +Tcl_PutEnv +Tcl_QueryTimeProc +Tcl_QueueEvent +Tcl_Read +Tcl_ReadChars +Tcl_ReadRaw +Tcl_Realloc +Tcl_ReapDetachedProcs +Tcl_RecordAndEval +Tcl_RecordAndEvalObj +Tcl_RegExpCompile +Tcl_RegExpExec +Tcl_RegExpExecObj +Tcl_RegExpGetInfo +Tcl_RegExpMatch +Tcl_RegExpMatchObj +Tcl_RegExpRange +Tcl_RegisterChannel +Tcl_RegisterConfig +Tcl_RegisterObjType +Tcl_Release +Tcl_ResetResult +Tcl_RestoreInterpState +Tcl_RestoreResult +Tcl_SaveInterpState +Tcl_SaveResult +Tcl_ScanCountedElement +Tcl_ScanElement +Tcl_Seek +Tcl_ServiceAll +Tcl_ServiceEvent +Tcl_SetAssocData +Tcl_SetBignumObj +Tcl_SetBooleanObj +Tcl_SetByteArrayLength +Tcl_SetByteArrayObj +Tcl_SetChannelBufferSize +Tcl_SetChannelError +Tcl_SetChannelErrorInterp +Tcl_SetChannelOption +Tcl_SetCommandInfo +Tcl_SetCommandInfoFromToken +Tcl_SetDefaultEncodingDir +Tcl_SetDoubleObj +Tcl_SetEncodingSearchPath +Tcl_SetEnsembleFlags +Tcl_SetEnsembleMappingDict +Tcl_SetEnsembleSubcommandList +Tcl_SetEnsembleUnknownHandler +Tcl_SetErrno +Tcl_SetErrorCode +Tcl_SetErrorCodeVA +Tcl_SetExitProc +Tcl_SetHashValue +Tcl_SetIntObj +Tcl_SetListObj +Tcl_SetLongObj +Tcl_SetMainLoop +Tcl_SetMaxBlockTime +Tcl_SetNamespaceUnknownHandler +Tcl_SetObjErrorCode +Tcl_SetObjLength +Tcl_SetObjResult +Tcl_SetPanicProc +Tcl_SetRecursionLimit +Tcl_SetResult +Tcl_SetReturnOptions +Tcl_SetServiceMode +Tcl_SetStdChannel +Tcl_SetStringObj +Tcl_SetSystemEncoding +Tcl_SetTimeProc +Tcl_SetTimer +Tcl_SetUnicodeObj +Tcl_SetVar +Tcl_SetVar2 +Tcl_SetVar2Ex +Tcl_SetWideIntObj +Tcl_SignalId +Tcl_SignalMsg +Tcl_Sleep +Tcl_SourceRCFile +Tcl_SpliceChannel +Tcl_SplitList +Tcl_SplitPath +Tcl_StackChannel +Tcl_StandardChannels +Tcl_Stat +Tcl_StaticPackage +Tcl_StringCaseMatch +Tcl_StringMatch +Tcl_SubstObj +Tcl_TakeBignumFromObj +Tcl_Tell +Tcl_ThreadAlert +Tcl_ThreadQueueEvent +Tcl_TraceCommand +Tcl_TraceVar +Tcl_TraceVar2 +Tcl_TranslateFileName +Tcl_TruncateChannel +Tcl_Ungets +Tcl_UniChar +Tcl_UniCharAtIndex +Tcl_UniCharCaseMatch +Tcl_UniCharIsAlnum +Tcl_UniCharIsAlpha +Tcl_UniCharIsControl +Tcl_UniCharIsDigit +Tcl_UniCharIsGraph +Tcl_UniCharIsLower +Tcl_UniCharIsPrint +Tcl_UniCharIsPunct +Tcl_UniCharIsSpace +Tcl_UniCharIsUpper +Tcl_UniCharIsWordChar +Tcl_UniCharLen +Tcl_UniCharNcasecmp +Tcl_UniCharNcmp +Tcl_UniCharToLower +Tcl_UniCharToTitle +Tcl_UniCharToUpper +Tcl_UniCharToUtf +Tcl_UniCharToUtfDString +Tcl_UnlinkVar +Tcl_UnregisterChannel +Tcl_UnsetVar +Tcl_UnsetVar2 +Tcl_UnstackChannel +Tcl_UntraceCommand +Tcl_UntraceVar +Tcl_UntraceVar2 +Tcl_UpdateLinkedVar +Tcl_UpVar +Tcl_UpVar2 +Tcl_UtfAtIndex +Tcl_UtfBackslash +Tcl_UtfCharComplete +Tcl_UtfFindFirst +Tcl_UtfFindLast +Tcl_UtfNext +Tcl_UtfPrev +Tcl_UtfToExternal +Tcl_UtfToExternalDString +Tcl_UtfToLower +Tcl_UtfToTitle +Tcl_UtfToUniChar +Tcl_UtfToUniCharDString +Tcl_UtfToUpper +Tcl_ValidateAllMemory +Tcl_VarEval +Tcl_VarEvalVA +Tcl_VarTraceInfo +Tcl_VarTraceInfo2 +Tcl_WaitForEvent +Tcl_WaitPid +Tcl_WinTCharToUtf +Tcl_WinUtfToTChar +Tcl_Write +Tcl_WriteChars +Tcl_WriteObj +Tcl_WriteRaw +Tcl_WrongNumArgs +term_attrs +term_attrs_sp +termattrs_sp +termname_sp +term_variables +text_height +text_length +textout_centre_ex +textout_ex +textout_justify_ex +textout_right_ex +textprintf_centre_ex +textprintf_ex +textprintf_justify_ex +textprintf_right_ex +tgetent_sp +tgetflag_sp +tgetnum_sp +tgetstr_sp +three_finger_flag +tigetflag_sp +tigetnum_sp +tigetstr_sp +tmpnam_r +top_panel +top_row +tputs_sp +_traceattr2 +_traceattr +_tracecchar_t2 +_tracecchar_t +_tracechar +_tracechtype2 +_tracechtype +_tracedump +_tracef +_tracemouse +transpose_font +triangle3d_f +ttyname_r +typeahead_sp +TYPE_ALNUM +TYPE_ALPHA +TYPE_ENUM +TYPE_INTEGER +TYPE_IPV4 +TYPE_NUMERIC +TYPE_REGEXP +uconvert_ascii +uconvert_size +uconvert_toascii +unctrl_sp +ungetch_sp +ungetmouse_sp +unget_wch +unget_wch_sp +unload_datafile +unload_datafile_object +unlocked_stdio +unpost_form +unpost_menu +unscare_mouse +unselect_palette +update_dialog +update_menu +update_panels +update_panels_sp +use_default_colors +use_default_colors_sp +use_env +use_env_sp +use_extended_names +use_legacy_coding +use_legacy_coding_sp +use_screen +use_tioctl +use_window +_ustrdup +ustrtok_r +uwidth_max +V3D_f +va_arg +va_copy +va_end +va_start +vector_length +vector_length_f +vid_attr +vid_attr_sp +vidattr_sp +vid_puts +vid_puts_sp +vidputs_sp +VIRTUAL_H +VIRTUAL_W +vline_set +voice_check +voice_get_frequency +voice_get_pan +voice_get_position +voice_get_volume +voice_ramp_volume +voice_set_echo +voice_set_frequency +voice_set_pan +voice_set_playmode +voice_set_position +voice_set_priority +voice_set_tremolo +voice_set_vibrato +voice_set_volume +voice_start +voice_stop +voice_stop_frequency_sweep +voice_stop_pan_sweep +voice_stop_volumeramp +voice_sweep_frequency +voice_sweep_pan +vw_printw +vw_scanw +wadd_wch +wadd_wchnstr +wadd_wchstr +wattr_get +wattr_off +wattr_on +wattr_set +wborder_set +wcolor_set +wecho_wchar +wgetn_wstr +wget_wch +wget_wstr +whline_set +wins_nwstr +wins_wch +wins_wstr +win_wch +win_wchnstr +win_wchstr +wmouse_trafo +wunctrl_sp +wvline_set +xcb_alloc_color +xcb_alloc_color_cells +xcb_alloc_color_cells_masks +xcb_alloc_color_cells_masks_end +xcb_alloc_color_cells_masks_length +xcb_alloc_color_cells_pixels +xcb_alloc_color_cells_pixels_end +xcb_alloc_color_cells_pixels_length +xcb_alloc_color_cells_reply +xcb_alloc_color_cells_unchecked +xcb_alloc_color_planes +xcb_alloc_color_planes_pixels +xcb_alloc_color_planes_pixels_end +xcb_alloc_color_planes_pixels_length +xcb_alloc_color_planes_reply +xcb_alloc_color_planes_unchecked +xcb_alloc_color_reply +xcb_alloc_color_unchecked +xcb_alloc_named_color +xcb_alloc_named_color_reply +xcb_alloc_named_color_unchecked +xcb_allow_events +xcb_allow_events_checked +xcb_bell +xcb_bell_checked +xcb_big_requests_enable +xcb_big_requests_enable_reply +xcb_big_requests_enable_unchecked +xcb_button_press_event_t +xcb_button_release_event_t +xcb_change_active_pointer_grab +xcb_change_active_pointer_grab_checked +xcb_change_gc +xcb_change_gc_checked +xcb_change_hosts +xcb_change_hosts_checked +xcb_change_keyboard_control +xcb_change_keyboard_control_checked +xcb_change_keyboard_mapping +xcb_change_keyboard_mapping_checked +xcb_change_pointer_control +xcb_change_pointer_control_checked +xcb_change_property +xcb_change_property_checked +xcb_change_save_set +xcb_change_save_set_checked +xcb_change_window_attributes +xcb_change_window_attributes_checked +xcb_circulate_notify_event_t +xcb_circulate_request_event_t +xcb_circulate_window +xcb_circulate_window_checked +xcb_clear_area +xcb_clear_area_checked +xcb_client_message_event_t +xcb_close_font +xcb_close_font_checked +xcb_colormap_notify_event_t +xcb_composite_create_region_from_border_clip +xcb_composite_create_region_from_border_clip_checked +xcb_composite_get_overlay_window +xcb_composite_get_overlay_window_reply +xcb_composite_get_overlay_window_unchecked +xcb_composite_name_window_pixmap +xcb_composite_name_window_pixmap_checked +xcb_composite_query_version +xcb_composite_query_version_reply +xcb_composite_query_version_unchecked +xcb_composite_redirect_subwindows +xcb_composite_redirect_subwindows_checked +xcb_composite_redirect_window +xcb_composite_redirect_window_checked +xcb_composite_release_overlay_window +xcb_composite_release_overlay_window_checked +xcb_composite_unredirect_subwindows +xcb_composite_unredirect_subwindows_checked +xcb_composite_unredirect_window +xcb_composite_unredirect_window_checked +xcb_configure_notify_event_t +xcb_configure_request_event_t +xcb_configure_window +xcb_configure_window_checked +xcb_convert_selection +xcb_convert_selection_checked +xcb_copy_area +xcb_copy_area_checked +xcb_copy_colormap_and_free +xcb_copy_colormap_and_free_checked +xcb_copy_gc +xcb_copy_gc_checked +xcb_copy_plane +xcb_copy_plane_checked +xcb_create_colormap +xcb_create_colormap_checked +xcb_create_cursor +xcb_create_cursor_checked +xcb_create_gc +xcb_create_gc_checked +xcb_create_glyph_cursor +xcb_create_glyph_cursor_checked +xcb_create_notify_event_t +xcb_create_pixmap +xcb_create_pixmap_checked +xcb_create_window +xcb_create_window_checked +xcb_damage_add +xcb_damage_add_checked +xcb_damage_create +xcb_damage_create_checked +xcb_damage_destroy +xcb_damage_destroy_checked +xcb_damage_notify_event_t +xcb_damage_query_version +xcb_damage_query_version_reply +xcb_damage_query_version_unchecked +xcb_damage_subtract +xcb_damage_subtract_checked +xcb_delete_property +xcb_delete_property_checked +xcb_destroy_notify_event_t +xcb_destroy_subwindows +xcb_destroy_subwindows_checked +xcb_destroy_window +xcb_destroy_window_checked +xcb_dpms_capable +xcb_dpms_capable_reply +xcb_dpms_capable_unchecked +xcb_dpms_disable +xcb_dpms_disable_checked +xcb_dpms_enable +xcb_dpms_enable_checked +xcb_dpms_force_level +xcb_dpms_force_level_checked +xcb_dpms_get_timeouts +xcb_dpms_get_timeouts_reply +xcb_dpms_get_timeouts_unchecked +xcb_dpms_get_version +xcb_dpms_get_version_reply +xcb_dpms_get_version_unchecked +xcb_dpms_info +xcb_dpms_info_reply +xcb_dpms_info_unchecked +xcb_dpms_set_timeouts +xcb_dpms_set_timeouts_checked +xcb_dri2_authenticate +xcb_dri2_authenticate_reply +xcb_dri2_authenticate_unchecked +xcb_dri2_buffer_swap_complete_event_t +xcb_dri2_connect +xcb_dri2_connect_alignment_pad +xcb_dri2_connect_alignment_pad_end +xcb_dri2_connect_alignment_pad_length +xcb_dri2_connect_device_name +xcb_dri2_connect_device_name_end +xcb_dri2_connect_device_name_length +xcb_dri2_connect_driver_name +xcb_dri2_connect_driver_name_end +xcb_dri2_connect_driver_name_length +xcb_dri2_connect_reply +xcb_dri2_connect_unchecked +xcb_dri2_copy_region +xcb_dri2_copy_region_reply +xcb_dri2_copy_region_unchecked +xcb_dri2_create_drawable +xcb_dri2_create_drawable_checked +xcb_dri2_destroy_drawable +xcb_dri2_destroy_drawable_checked +xcb_dri2_get_buffers +xcb_dri2_get_buffers_buffers +xcb_dri2_get_buffers_buffers_iterator +xcb_dri2_get_buffers_buffers_length +xcb_dri2_get_buffers_reply +xcb_dri2_get_buffers_unchecked +xcb_dri2_get_buffers_with_format +xcb_dri2_get_buffers_with_format_buffers +xcb_dri2_get_buffers_with_format_buffers_iterator +xcb_dri2_get_buffers_with_format_buffers_length +xcb_dri2_get_buffers_with_format_reply +xcb_dri2_get_buffers_with_format_unchecked +xcb_dri2_get_msc +xcb_dri2_get_msc_reply +xcb_dri2_get_msc_unchecked +xcb_dri2_get_param +xcb_dri2_get_param_reply +xcb_dri2_get_param_unchecked +xcb_dri2_invalidate_buffers_event_t +xcb_dri2_query_version +xcb_dri2_query_version_reply +xcb_dri2_query_version_unchecked +xcb_dri2_swap_buffers +xcb_dri2_swap_buffers_reply +xcb_dri2_swap_buffers_unchecked +xcb_dri2_swap_interval +xcb_dri2_swap_interval_checked +xcb_dri2_wait_msc +xcb_dri2_wait_msc_reply +xcb_dri2_wait_msc_unchecked +xcb_dri2_wait_sbc +xcb_dri2_wait_sbc_reply +xcb_dri2_wait_sbc_unchecked +xcb_enter_notify_event_t +xcb_expose_event_t +xcb_fill_poly +xcb_fill_poly_checked +xcb_focus_in_event_t +xcb_focus_out_event_t +xcb_force_screen_saver +xcb_force_screen_saver_checked +xcb_free_colormap +xcb_free_colormap_checked +xcb_free_colors +xcb_free_colors_checked +xcb_free_cursor +xcb_free_cursor_checked +xcb_free_gc +xcb_free_gc_checked +xcb_free_pixmap +xcb_free_pixmap_checked +xcb_get_atom_name +xcb_get_atom_name_name +xcb_get_atom_name_name_end +xcb_get_atom_name_name_length +xcb_get_atom_name_reply +xcb_get_atom_name_unchecked +xcb_get_font_path +xcb_get_font_path_path_iterator +xcb_get_font_path_path_length +xcb_get_font_path_reply +xcb_get_font_path_unchecked +xcb_get_geometry +xcb_get_geometry_reply +xcb_get_geometry_unchecked +xcb_get_image +xcb_get_image_data +xcb_get_image_data_end +xcb_get_image_data_length +xcb_get_image_reply +xcb_get_image_unchecked +xcb_get_input_focus +xcb_get_input_focus_reply +xcb_get_input_focus_unchecked +xcb_get_keyboard_control +xcb_get_keyboard_control_reply +xcb_get_keyboard_control_unchecked +xcb_get_keyboard_mapping +xcb_get_keyboard_mapping_keysyms +xcb_get_keyboard_mapping_keysyms_end +xcb_get_keyboard_mapping_keysyms_length +xcb_get_keyboard_mapping_reply +xcb_get_keyboard_mapping_unchecked +xcb_get_modifier_mapping +xcb_get_modifier_mapping_keycodes +xcb_get_modifier_mapping_keycodes_end +xcb_get_modifier_mapping_keycodes_length +xcb_get_modifier_mapping_reply +xcb_get_modifier_mapping_unchecked +xcb_get_motion_events +xcb_get_motion_events_events +xcb_get_motion_events_events_iterator +xcb_get_motion_events_events_length +xcb_get_motion_events_reply +xcb_get_motion_events_unchecked +xcb_get_pointer_control +xcb_get_pointer_control_reply +xcb_get_pointer_control_unchecked +xcb_get_pointer_mapping +xcb_get_pointer_mapping_map +xcb_get_pointer_mapping_map_end +xcb_get_pointer_mapping_map_length +xcb_get_pointer_mapping_reply +xcb_get_pointer_mapping_unchecked +xcb_get_property +xcb_get_property_reply +xcb_get_property_unchecked +xcb_get_property_value +xcb_get_property_value_end +xcb_get_property_value_length +xcb_get_screen_saver +xcb_get_screen_saver_reply +xcb_get_screen_saver_unchecked +xcb_get_selection_owner +xcb_get_selection_owner_reply +xcb_get_selection_owner_unchecked +xcb_get_window_attributes +xcb_get_window_attributes_reply +xcb_get_window_attributes_unchecked +xcb_glx_are_textures_resident +xcb_glx_are_textures_resident_data +xcb_glx_are_textures_resident_data_end +xcb_glx_are_textures_resident_data_length +xcb_glx_are_textures_resident_reply +xcb_glx_are_textures_resident_unchecked +xcb_glx_change_drawable_attributes +xcb_glx_change_drawable_attributes_checked +xcb_glx_client_info +xcb_glx_client_info_checked +xcb_glx_copy_context +xcb_glx_copy_context_checked +xcb_glx_create_context +xcb_glx_create_context_attribs_arb +xcb_glx_create_context_attribs_arb_checked +xcb_glx_create_context_checked +xcb_glx_create_glx_pixmap +xcb_glx_create_glx_pixmap_checked +xcb_glx_create_new_context +xcb_glx_create_new_context_checked +xcb_glx_create_pbuffer +xcb_glx_create_pbuffer_checked +xcb_glx_create_pixmap +xcb_glx_create_pixmap_checked +xcb_glx_create_window +xcb_glx_create_window_checked +xcb_glx_delete_lists +xcb_glx_delete_lists_checked +xcb_glx_delete_queries_arb +xcb_glx_delete_queries_arb_checked +xcb_glx_delete_textures +xcb_glx_delete_textures_checked +xcb_glx_delete_window +xcb_glx_delete_window_checked +xcb_glx_destroy_context +xcb_glx_destroy_context_checked +xcb_glx_destroy_glx_pixmap +xcb_glx_destroy_glx_pixmap_checked +xcb_glx_destroy_pbuffer +xcb_glx_destroy_pbuffer_checked +xcb_glx_destroy_pixmap +xcb_glx_destroy_pixmap_checked +xcb_glx_end_list +xcb_glx_end_list_checked +xcb_glx_feedback_buffer +xcb_glx_feedback_buffer_checked +xcb_glx_finish +xcb_glx_finish_reply +xcb_glx_finish_unchecked +xcb_glx_flush +xcb_glx_flush_checked +xcb_glx_gen_lists +xcb_glx_gen_lists_reply +xcb_glx_gen_lists_unchecked +xcb_glx_gen_queries_arb +xcb_glx_gen_queries_arb_data +xcb_glx_gen_queries_arb_data_end +xcb_glx_gen_queries_arb_data_length +xcb_glx_gen_queries_arb_reply +xcb_glx_gen_queries_arb_unchecked +xcb_glx_gen_textures +xcb_glx_gen_textures_data +xcb_glx_gen_textures_data_end +xcb_glx_gen_textures_data_length +xcb_glx_gen_textures_reply +xcb_glx_gen_textures_unchecked +xcb_glx_get_booleanv +xcb_glx_get_booleanv_data +xcb_glx_get_booleanv_data_end +xcb_glx_get_booleanv_data_length +xcb_glx_get_booleanv_reply +xcb_glx_get_booleanv_unchecked +xcb_glx_get_clip_plane +xcb_glx_get_clip_plane_data +xcb_glx_get_clip_plane_data_end +xcb_glx_get_clip_plane_data_length +xcb_glx_get_clip_plane_reply +xcb_glx_get_clip_plane_unchecked +xcb_glx_get_color_table +xcb_glx_get_color_table_data +xcb_glx_get_color_table_data_end +xcb_glx_get_color_table_data_length +xcb_glx_get_color_table_parameterfv +xcb_glx_get_color_table_parameterfv_data +xcb_glx_get_color_table_parameterfv_data_end +xcb_glx_get_color_table_parameterfv_data_length +xcb_glx_get_color_table_parameterfv_reply +xcb_glx_get_color_table_parameterfv_unchecked +xcb_glx_get_color_table_parameteriv +xcb_glx_get_color_table_parameteriv_data +xcb_glx_get_color_table_parameteriv_data_end +xcb_glx_get_color_table_parameteriv_data_length +xcb_glx_get_color_table_parameteriv_reply +xcb_glx_get_color_table_parameteriv_unchecked +xcb_glx_get_color_table_reply +xcb_glx_get_color_table_unchecked +xcb_glx_get_compressed_tex_image_arb +xcb_glx_get_compressed_tex_image_arb_data +xcb_glx_get_compressed_tex_image_arb_data_end +xcb_glx_get_compressed_tex_image_arb_data_length +xcb_glx_get_compressed_tex_image_arb_reply +xcb_glx_get_compressed_tex_image_arb_unchecked +xcb_glx_get_convolution_filter +xcb_glx_get_convolution_filter_data +xcb_glx_get_convolution_filter_data_end +xcb_glx_get_convolution_filter_data_length +xcb_glx_get_convolution_filter_reply +xcb_glx_get_convolution_filter_unchecked +xcb_glx_get_convolution_parameterfv +xcb_glx_get_convolution_parameterfv_data +xcb_glx_get_convolution_parameterfv_data_end +xcb_glx_get_convolution_parameterfv_data_length +xcb_glx_get_convolution_parameterfv_reply +xcb_glx_get_convolution_parameterfv_unchecked +xcb_glx_get_convolution_parameteriv +xcb_glx_get_convolution_parameteriv_data +xcb_glx_get_convolution_parameteriv_data_end +xcb_glx_get_convolution_parameteriv_data_length +xcb_glx_get_convolution_parameteriv_reply +xcb_glx_get_convolution_parameteriv_unchecked +xcb_glx_get_doublev +xcb_glx_get_doublev_data +xcb_glx_get_doublev_data_end +xcb_glx_get_doublev_data_length +xcb_glx_get_doublev_reply +xcb_glx_get_doublev_unchecked +xcb_glx_get_drawable_attributes +xcb_glx_get_drawable_attributes_attribs +xcb_glx_get_drawable_attributes_attribs_end +xcb_glx_get_drawable_attributes_attribs_length +xcb_glx_get_drawable_attributes_reply +xcb_glx_get_drawable_attributes_unchecked +xcb_glx_get_error +xcb_glx_get_error_reply +xcb_glx_get_error_unchecked +xcb_glx_get_fb_configs +xcb_glx_get_fb_configs_property_list +xcb_glx_get_fb_configs_property_list_end +xcb_glx_get_fb_configs_property_list_length +xcb_glx_get_fb_configs_reply +xcb_glx_get_fb_configs_unchecked +xcb_glx_get_floatv +xcb_glx_get_floatv_data +xcb_glx_get_floatv_data_end +xcb_glx_get_floatv_data_length +xcb_glx_get_floatv_reply +xcb_glx_get_floatv_unchecked +xcb_glx_get_histogram +xcb_glx_get_histogram_data +xcb_glx_get_histogram_data_end +xcb_glx_get_histogram_data_length +xcb_glx_get_histogram_parameterfv +xcb_glx_get_histogram_parameterfv_data +xcb_glx_get_histogram_parameterfv_data_end +xcb_glx_get_histogram_parameterfv_data_length +xcb_glx_get_histogram_parameterfv_reply +xcb_glx_get_histogram_parameterfv_unchecked +xcb_glx_get_histogram_parameteriv +xcb_glx_get_histogram_parameteriv_data +xcb_glx_get_histogram_parameteriv_data_end +xcb_glx_get_histogram_parameteriv_data_length +xcb_glx_get_histogram_parameteriv_reply +xcb_glx_get_histogram_parameteriv_unchecked +xcb_glx_get_histogram_reply +xcb_glx_get_histogram_unchecked +xcb_glx_get_integerv +xcb_glx_get_integerv_data +xcb_glx_get_integerv_data_end +xcb_glx_get_integerv_data_length +xcb_glx_get_integerv_reply +xcb_glx_get_integerv_unchecked +xcb_glx_get_lightfv +xcb_glx_get_lightfv_data +xcb_glx_get_lightfv_data_end +xcb_glx_get_lightfv_data_length +xcb_glx_get_lightfv_reply +xcb_glx_get_lightfv_unchecked +xcb_glx_get_lightiv +xcb_glx_get_lightiv_data +xcb_glx_get_lightiv_data_end +xcb_glx_get_lightiv_data_length +xcb_glx_get_lightiv_reply +xcb_glx_get_lightiv_unchecked +xcb_glx_get_mapdv +xcb_glx_get_mapdv_data +xcb_glx_get_mapdv_data_end +xcb_glx_get_mapdv_data_length +xcb_glx_get_mapdv_reply +xcb_glx_get_mapdv_unchecked +xcb_glx_get_mapfv +xcb_glx_get_mapfv_data +xcb_glx_get_mapfv_data_end +xcb_glx_get_mapfv_data_length +xcb_glx_get_mapfv_reply +xcb_glx_get_mapfv_unchecked +xcb_glx_get_mapiv +xcb_glx_get_mapiv_data +xcb_glx_get_mapiv_data_end +xcb_glx_get_mapiv_data_length +xcb_glx_get_mapiv_reply +xcb_glx_get_mapiv_unchecked +xcb_glx_get_materialfv +xcb_glx_get_materialfv_data +xcb_glx_get_materialfv_data_end +xcb_glx_get_materialfv_data_length +xcb_glx_get_materialfv_reply +xcb_glx_get_materialfv_unchecked +xcb_glx_get_materialiv +xcb_glx_get_materialiv_data +xcb_glx_get_materialiv_data_end +xcb_glx_get_materialiv_data_length +xcb_glx_get_materialiv_reply +xcb_glx_get_materialiv_unchecked +xcb_glx_get_minmax +xcb_glx_get_minmax_data +xcb_glx_get_minmax_data_end +xcb_glx_get_minmax_data_length +xcb_glx_get_minmax_parameterfv +xcb_glx_get_minmax_parameterfv_data +xcb_glx_get_minmax_parameterfv_data_end +xcb_glx_get_minmax_parameterfv_data_length +xcb_glx_get_minmax_parameterfv_reply +xcb_glx_get_minmax_parameterfv_unchecked +xcb_glx_get_minmax_parameteriv +xcb_glx_get_minmax_parameteriv_data +xcb_glx_get_minmax_parameteriv_data_end +xcb_glx_get_minmax_parameteriv_data_length +xcb_glx_get_minmax_parameteriv_reply +xcb_glx_get_minmax_parameteriv_unchecked +xcb_glx_get_minmax_reply +xcb_glx_get_minmax_unchecked +xcb_glx_get_pixel_mapfv +xcb_glx_get_pixel_mapfv_data +xcb_glx_get_pixel_mapfv_data_end +xcb_glx_get_pixel_mapfv_data_length +xcb_glx_get_pixel_mapfv_reply +xcb_glx_get_pixel_mapfv_unchecked +xcb_glx_get_pixel_mapuiv +xcb_glx_get_pixel_mapuiv_data +xcb_glx_get_pixel_mapuiv_data_end +xcb_glx_get_pixel_mapuiv_data_length +xcb_glx_get_pixel_mapuiv_reply +xcb_glx_get_pixel_mapuiv_unchecked +xcb_glx_get_pixel_mapusv +xcb_glx_get_pixel_mapusv_data +xcb_glx_get_pixel_mapusv_data_end +xcb_glx_get_pixel_mapusv_data_length +xcb_glx_get_pixel_mapusv_reply +xcb_glx_get_pixel_mapusv_unchecked +xcb_glx_get_polygon_stipple +xcb_glx_get_polygon_stipple_data +xcb_glx_get_polygon_stipple_data_end +xcb_glx_get_polygon_stipple_data_length +xcb_glx_get_polygon_stipple_reply +xcb_glx_get_polygon_stipple_unchecked +xcb_glx_get_queryiv_arb +xcb_glx_get_queryiv_arb_data +xcb_glx_get_queryiv_arb_data_end +xcb_glx_get_queryiv_arb_data_length +xcb_glx_get_queryiv_arb_reply +xcb_glx_get_queryiv_arb_unchecked +xcb_glx_get_query_objectiv_arb +xcb_glx_get_query_objectiv_arb_data +xcb_glx_get_query_objectiv_arb_data_end +xcb_glx_get_query_objectiv_arb_data_length +xcb_glx_get_query_objectiv_arb_reply +xcb_glx_get_query_objectiv_arb_unchecked +xcb_glx_get_query_objectuiv_arb +xcb_glx_get_query_objectuiv_arb_data +xcb_glx_get_query_objectuiv_arb_data_end +xcb_glx_get_query_objectuiv_arb_data_length +xcb_glx_get_query_objectuiv_arb_reply +xcb_glx_get_query_objectuiv_arb_unchecked +xcb_glx_get_separable_filter +xcb_glx_get_separable_filter_reply +xcb_glx_get_separable_filter_rows_and_cols +xcb_glx_get_separable_filter_rows_and_cols_end +xcb_glx_get_separable_filter_rows_and_cols_length +xcb_glx_get_separable_filter_unchecked +xcb_glx_get_string +xcb_glx_get_string_reply +xcb_glx_get_string_string +xcb_glx_get_string_string_end +xcb_glx_get_string_string_length +xcb_glx_get_string_unchecked +xcb_glx_get_tex_envfv +xcb_glx_get_tex_envfv_data +xcb_glx_get_tex_envfv_data_end +xcb_glx_get_tex_envfv_data_length +xcb_glx_get_tex_envfv_reply +xcb_glx_get_tex_envfv_unchecked +xcb_glx_get_tex_enviv +xcb_glx_get_tex_enviv_data +xcb_glx_get_tex_enviv_data_end +xcb_glx_get_tex_enviv_data_length +xcb_glx_get_tex_enviv_reply +xcb_glx_get_tex_enviv_unchecked +xcb_glx_get_tex_gendv +xcb_glx_get_tex_gendv_data +xcb_glx_get_tex_gendv_data_end +xcb_glx_get_tex_gendv_data_length +xcb_glx_get_tex_gendv_reply +xcb_glx_get_tex_gendv_unchecked +xcb_glx_get_tex_genfv +xcb_glx_get_tex_genfv_data +xcb_glx_get_tex_genfv_data_end +xcb_glx_get_tex_genfv_data_length +xcb_glx_get_tex_genfv_reply +xcb_glx_get_tex_genfv_unchecked +xcb_glx_get_tex_geniv +xcb_glx_get_tex_geniv_data +xcb_glx_get_tex_geniv_data_end +xcb_glx_get_tex_geniv_data_length +xcb_glx_get_tex_geniv_reply +xcb_glx_get_tex_geniv_unchecked +xcb_glx_get_tex_image +xcb_glx_get_tex_image_data +xcb_glx_get_tex_image_data_end +xcb_glx_get_tex_image_data_length +xcb_glx_get_tex_image_reply +xcb_glx_get_tex_image_unchecked +xcb_glx_get_tex_level_parameterfv +xcb_glx_get_tex_level_parameterfv_data +xcb_glx_get_tex_level_parameterfv_data_end +xcb_glx_get_tex_level_parameterfv_data_length +xcb_glx_get_tex_level_parameterfv_reply +xcb_glx_get_tex_level_parameterfv_unchecked +xcb_glx_get_tex_level_parameteriv +xcb_glx_get_tex_level_parameteriv_data +xcb_glx_get_tex_level_parameteriv_data_end +xcb_glx_get_tex_level_parameteriv_data_length +xcb_glx_get_tex_level_parameteriv_reply +xcb_glx_get_tex_level_parameteriv_unchecked +xcb_glx_get_tex_parameterfv +xcb_glx_get_tex_parameterfv_data +xcb_glx_get_tex_parameterfv_data_end +xcb_glx_get_tex_parameterfv_data_length +xcb_glx_get_tex_parameterfv_reply +xcb_glx_get_tex_parameterfv_unchecked +xcb_glx_get_tex_parameteriv +xcb_glx_get_tex_parameteriv_data +xcb_glx_get_tex_parameteriv_data_end +xcb_glx_get_tex_parameteriv_data_length +xcb_glx_get_tex_parameteriv_reply +xcb_glx_get_tex_parameteriv_unchecked +xcb_glx_get_visual_configs +xcb_glx_get_visual_configs_property_list +xcb_glx_get_visual_configs_property_list_end +xcb_glx_get_visual_configs_property_list_length +xcb_glx_get_visual_configs_reply +xcb_glx_get_visual_configs_unchecked +xcb_glx_is_direct +xcb_glx_is_direct_reply +xcb_glx_is_direct_unchecked +xcb_glx_is_list +xcb_glx_is_list_reply +xcb_glx_is_list_unchecked +xcb_glx_is_query_arb +xcb_glx_is_query_arb_reply +xcb_glx_is_query_arb_unchecked +xcb_glx_is_texture +xcb_glx_is_texture_reply +xcb_glx_is_texture_unchecked +xcb_glx_make_context_current +xcb_glx_make_context_current_reply +xcb_glx_make_context_current_unchecked +xcb_glx_make_current +xcb_glx_make_current_reply +xcb_glx_make_current_unchecked +xcb_glx_new_list +xcb_glx_new_list_checked +xcb_glx_pbuffer_clobber_event_t +xcb_glx_pixel_storef +xcb_glx_pixel_storef_checked +xcb_glx_pixel_storei +xcb_glx_pixel_storei_checked +xcb_glx_query_context +xcb_glx_query_context_attribs +xcb_glx_query_context_attribs_end +xcb_glx_query_context_attribs_length +xcb_glx_query_context_reply +xcb_glx_query_context_unchecked +xcb_glx_query_extensions_string +xcb_glx_query_extensions_string_reply +xcb_glx_query_extensions_string_unchecked +xcb_glx_query_server_string +xcb_glx_query_server_string_reply +xcb_glx_query_server_string_string +xcb_glx_query_server_string_string_end +xcb_glx_query_server_string_string_length +xcb_glx_query_server_string_unchecked +xcb_glx_query_version +xcb_glx_query_version_reply +xcb_glx_query_version_unchecked +xcb_glx_read_pixels +xcb_glx_read_pixels_data +xcb_glx_read_pixels_data_end +xcb_glx_read_pixels_data_length +xcb_glx_read_pixels_reply +xcb_glx_read_pixels_unchecked +xcb_glx_render +xcb_glx_render_checked +xcb_glx_render_large +xcb_glx_render_large_checked +xcb_glx_render_mode +xcb_glx_render_mode_data +xcb_glx_render_mode_data_end +xcb_glx_render_mode_data_length +xcb_glx_render_mode_reply +xcb_glx_render_mode_unchecked +xcb_glx_select_buffer +xcb_glx_select_buffer_checked +xcb_glx_set_client_info_2arb +xcb_glx_set_client_info_2arb_checked +xcb_glx_set_client_info_arb +xcb_glx_set_client_info_arb_checked +xcb_glx_swap_buffers +xcb_glx_swap_buffers_checked +xcb_glx_use_x_font +xcb_glx_use_x_font_checked +xcb_glx_vendor_private +xcb_glx_vendor_private_checked +xcb_glx_vendor_private_with_reply +xcb_glx_vendor_private_with_reply_data_2 +xcb_glx_vendor_private_with_reply_data_2_end +xcb_glx_vendor_private_with_reply_data_2_length +xcb_glx_vendor_private_with_reply_reply +xcb_glx_vendor_private_with_reply_unchecked +xcb_glx_wait_gl +xcb_glx_wait_gl_checked +xcb_glx_wait_x +xcb_glx_wait_x_checked +xcb_grab_button +xcb_grab_button_checked +xcb_grab_key +xcb_grab_keyboard +xcb_grab_keyboard_reply +xcb_grab_keyboard_unchecked +xcb_grab_key_checked +xcb_grab_pointer +xcb_grab_pointer_reply +xcb_grab_pointer_unchecked +xcb_grab_server +xcb_grab_server_checked +xcb_graphics_exposure_event_t +xcb_gravity_notify_event_t +xcb_image_text_16 +xcb_image_text_16_checked +xcb_image_text_8 +xcb_image_text_8_checked +xcb_input_allow_device_events +xcb_input_allow_device_events_checked +xcb_input_change_device_dont_propagate_list +xcb_input_change_device_dont_propagate_list_checked +xcb_input_change_device_key_mapping +xcb_input_change_device_key_mapping_checked +xcb_input_change_device_notify_event_t +xcb_input_change_keyboard_device +xcb_input_change_keyboard_device_reply +xcb_input_change_keyboard_device_unchecked +xcb_input_change_pointer_device +xcb_input_change_pointer_device_reply +xcb_input_change_pointer_device_unchecked +xcb_input_close_device +xcb_input_close_device_checked +xcb_input_device_bell +xcb_input_device_bell_checked +xcb_input_device_button_press_event_t +xcb_input_device_button_release_event_t +xcb_input_device_button_state_notify_event_t +xcb_input_device_key_press_event_t +xcb_input_device_key_release_event_t +xcb_input_device_key_state_notify_event_t +xcb_input_device_mapping_notify_event_t +xcb_input_device_motion_notify_event_t +xcb_input_device_presence_notify_event_t +xcb_input_device_state_notify_event_t +xcb_input_device_valuator_event_t +xcb_input_focus_in_event_t +xcb_input_focus_out_event_t +xcb_input_get_device_button_mapping +xcb_input_get_device_button_mapping_map +xcb_input_get_device_button_mapping_map_end +xcb_input_get_device_button_mapping_map_length +xcb_input_get_device_button_mapping_reply +xcb_input_get_device_button_mapping_unchecked +xcb_input_get_device_control +xcb_input_get_device_control_reply +xcb_input_get_device_control_unchecked +xcb_input_get_device_dont_propagate_list +xcb_input_get_device_dont_propagate_list_classes +xcb_input_get_device_dont_propagate_list_classes_end +xcb_input_get_device_dont_propagate_list_classes_length +xcb_input_get_device_dont_propagate_list_reply +xcb_input_get_device_dont_propagate_list_unchecked +xcb_input_get_device_focus +xcb_input_get_device_focus_reply +xcb_input_get_device_focus_unchecked +xcb_input_get_device_key_mapping +xcb_input_get_device_key_mapping_keysyms +xcb_input_get_device_key_mapping_keysyms_end +xcb_input_get_device_key_mapping_keysyms_length +xcb_input_get_device_key_mapping_reply +xcb_input_get_device_key_mapping_unchecked +xcb_input_get_device_modifier_mapping +xcb_input_get_device_modifier_mapping_keymaps +xcb_input_get_device_modifier_mapping_keymaps_end +xcb_input_get_device_modifier_mapping_keymaps_length +xcb_input_get_device_modifier_mapping_reply +xcb_input_get_device_modifier_mapping_unchecked +xcb_input_get_device_motion_events +xcb_input_get_device_motion_events_reply +xcb_input_get_device_motion_events_unchecked +xcb_input_get_extension_version +xcb_input_get_extension_version_reply +xcb_input_get_extension_version_unchecked +xcb_input_get_feedback_control +xcb_input_get_feedback_control_reply +xcb_input_get_feedback_control_unchecked +xcb_input_get_selected_extension_events +xcb_input_get_selected_extension_events_all_classes +xcb_input_get_selected_extension_events_all_classes_end +xcb_input_get_selected_extension_events_all_classes_length +xcb_input_get_selected_extension_events_reply +xcb_input_get_selected_extension_events_this_classes +xcb_input_get_selected_extension_events_this_classes_end +xcb_input_get_selected_extension_events_this_classes_length +xcb_input_get_selected_extension_events_unchecked +xcb_input_grab_device +xcb_input_grab_device_button +xcb_input_grab_device_button_checked +xcb_input_grab_device_key +xcb_input_grab_device_key_checked +xcb_input_grab_device_reply +xcb_input_grab_device_unchecked +xcb_input_list_input_devices +xcb_input_list_input_devices_devices +xcb_input_list_input_devices_devices_iterator +xcb_input_list_input_devices_devices_length +xcb_input_list_input_devices_reply +xcb_input_list_input_devices_unchecked +xcb_input_open_device +xcb_input_open_device_class_info +xcb_input_open_device_class_info_iterator +xcb_input_open_device_class_info_length +xcb_input_open_device_reply +xcb_input_open_device_unchecked +xcb_input_proximity_in_event_t +xcb_input_proximity_out_event_t +xcb_input_query_device_state +xcb_input_query_device_state_reply +xcb_input_query_device_state_unchecked +xcb_input_select_extension_event +xcb_input_select_extension_event_checked +xcb_input_send_extension_event +xcb_input_send_extension_event_checked +xcb_input_set_device_button_mapping +xcb_input_set_device_button_mapping_reply +xcb_input_set_device_button_mapping_unchecked +xcb_input_set_device_focus +xcb_input_set_device_focus_checked +xcb_input_set_device_mode +xcb_input_set_device_mode_reply +xcb_input_set_device_mode_unchecked +xcb_input_set_device_modifier_mapping +xcb_input_set_device_modifier_mapping_reply +xcb_input_set_device_modifier_mapping_unchecked +xcb_input_set_device_valuators +xcb_input_set_device_valuators_reply +xcb_input_set_device_valuators_unchecked +xcb_input_ungrab_device +xcb_input_ungrab_device_button +xcb_input_ungrab_device_button_checked +xcb_input_ungrab_device_checked +xcb_input_ungrab_device_key +xcb_input_ungrab_device_key_checked +xcb_install_colormap +xcb_install_colormap_checked +xcb_intern_atom +xcb_intern_atom_reply +xcb_intern_atom_unchecked +xcb_keymap_notify_event_t +xcb_key_press_event_t +xcb_key_release_event_t +xcb_kill_client +xcb_kill_client_checked +xcb_leave_notify_event_t +xcb_list_extensions +xcb_list_extensions_names_iterator +xcb_list_extensions_names_length +xcb_list_extensions_reply +xcb_list_extensions_unchecked +xcb_list_fonts +xcb_list_fonts_names_iterator +xcb_list_fonts_names_length +xcb_list_fonts_reply +xcb_list_fonts_unchecked +xcb_list_fonts_with_info +xcb_list_fonts_with_info_name +xcb_list_fonts_with_info_name_end +xcb_list_fonts_with_info_name_length +xcb_list_fonts_with_info_properties +xcb_list_fonts_with_info_properties_iterator +xcb_list_fonts_with_info_properties_length +xcb_list_fonts_with_info_reply +xcb_list_fonts_with_info_unchecked +xcb_list_hosts +xcb_list_hosts_hosts_iterator +xcb_list_hosts_hosts_length +xcb_list_hosts_reply +xcb_list_hosts_unchecked +xcb_list_installed_colormaps +xcb_list_installed_colormaps_cmaps +xcb_list_installed_colormaps_cmaps_end +xcb_list_installed_colormaps_cmaps_length +xcb_list_installed_colormaps_reply +xcb_list_installed_colormaps_unchecked +xcb_list_properties +xcb_list_properties_atoms +xcb_list_properties_atoms_end +xcb_list_properties_atoms_length +xcb_list_properties_reply +xcb_list_properties_unchecked +xcb_lookup_color +xcb_lookup_color_reply +xcb_lookup_color_unchecked +xcb_map_notify_event_t +xcb_mapping_notify_event_t +xcb_map_request_event_t +xcb_map_subwindows +xcb_map_subwindows_checked +xcb_map_window +xcb_map_window_checked +xcb_motion_notify_event_t +xcb_no_exposure_event_t +xcb_no_operation +xcb_no_operation_checked +xcb_open_font +xcb_open_font_checked +xcb_poly_arc +xcb_poly_arc_checked +xcb_poly_fill_arc +xcb_poly_fill_arc_checked +xcb_poly_fill_rectangle +xcb_poly_fill_rectangle_checked +xcb_poly_line +xcb_poly_line_checked +xcb_poly_point +xcb_poly_point_checked +xcb_poly_rectangle +xcb_poly_rectangle_checked +xcb_poly_segment +xcb_poly_segment_checked +xcb_poly_text_16 +xcb_poly_text_16_checked +xcb_poly_text_8 +xcb_poly_text_8_checked +xcb_property_notify_event_t +xcb_put_image +xcb_put_image_checked +xcb_query_best_size +xcb_query_best_size_reply +xcb_query_best_size_unchecked +xcb_query_colors +xcb_query_colors_colors +xcb_query_colors_colors_iterator +xcb_query_colors_colors_length +xcb_query_colors_reply +xcb_query_colors_unchecked +xcb_query_extension +xcb_query_extension_reply +xcb_query_extension_unchecked +xcb_query_font +xcb_query_font_char_infos +xcb_query_font_char_infos_iterator +xcb_query_font_char_infos_length +xcb_query_font_properties +xcb_query_font_properties_iterator +xcb_query_font_properties_length +xcb_query_font_reply +xcb_query_font_unchecked +xcb_query_keymap +xcb_query_keymap_reply +xcb_query_keymap_unchecked +xcb_query_pointer +xcb_query_pointer_reply +xcb_query_pointer_unchecked +xcb_query_text_extents +xcb_query_text_extents_reply +xcb_query_text_extents_unchecked +xcb_query_tree +xcb_query_tree_children +xcb_query_tree_children_end +xcb_query_tree_children_length +xcb_query_tree_reply +xcb_query_tree_unchecked +xcb_randr_add_output_mode +xcb_randr_add_output_mode_checked +xcb_randr_change_output_property +xcb_randr_change_output_property_checked +xcb_randr_configure_output_property +xcb_randr_configure_output_property_checked +xcb_randr_create_mode +xcb_randr_create_mode_reply +xcb_randr_create_mode_unchecked +xcb_randr_delete_output_mode +xcb_randr_delete_output_mode_checked +xcb_randr_delete_output_property +xcb_randr_delete_output_property_checked +xcb_randr_destroy_mode +xcb_randr_destroy_mode_checked +xcb_randr_get_crtc_gamma +xcb_randr_get_crtc_gamma_blue +xcb_randr_get_crtc_gamma_blue_end +xcb_randr_get_crtc_gamma_blue_length +xcb_randr_get_crtc_gamma_green +xcb_randr_get_crtc_gamma_green_end +xcb_randr_get_crtc_gamma_green_length +xcb_randr_get_crtc_gamma_red +xcb_randr_get_crtc_gamma_red_end +xcb_randr_get_crtc_gamma_red_length +xcb_randr_get_crtc_gamma_reply +xcb_randr_get_crtc_gamma_size +xcb_randr_get_crtc_gamma_size_reply +xcb_randr_get_crtc_gamma_size_unchecked +xcb_randr_get_crtc_gamma_unchecked +xcb_randr_get_crtc_info +xcb_randr_get_crtc_info_outputs +xcb_randr_get_crtc_info_outputs_end +xcb_randr_get_crtc_info_outputs_length +xcb_randr_get_crtc_info_possible +xcb_randr_get_crtc_info_possible_end +xcb_randr_get_crtc_info_possible_length +xcb_randr_get_crtc_info_reply +xcb_randr_get_crtc_info_unchecked +xcb_randr_get_crtc_transform +xcb_randr_get_crtc_transform_current_filter_name +xcb_randr_get_crtc_transform_current_filter_name_end +xcb_randr_get_crtc_transform_current_filter_name_length +xcb_randr_get_crtc_transform_current_params +xcb_randr_get_crtc_transform_current_params_end +xcb_randr_get_crtc_transform_current_params_length +xcb_randr_get_crtc_transform_pending_filter_name +xcb_randr_get_crtc_transform_pending_filter_name_end +xcb_randr_get_crtc_transform_pending_filter_name_length +xcb_randr_get_crtc_transform_pending_params +xcb_randr_get_crtc_transform_pending_params_end +xcb_randr_get_crtc_transform_pending_params_length +xcb_randr_get_crtc_transform_reply +xcb_randr_get_crtc_transform_unchecked +xcb_randr_get_output_info +xcb_randr_get_output_info_clones +xcb_randr_get_output_info_clones_end +xcb_randr_get_output_info_clones_length +xcb_randr_get_output_info_crtcs +xcb_randr_get_output_info_crtcs_end +xcb_randr_get_output_info_crtcs_length +xcb_randr_get_output_info_modes +xcb_randr_get_output_info_modes_end +xcb_randr_get_output_info_modes_length +xcb_randr_get_output_info_name +xcb_randr_get_output_info_name_end +xcb_randr_get_output_info_name_length +xcb_randr_get_output_info_reply +xcb_randr_get_output_info_unchecked +xcb_randr_get_output_primary +xcb_randr_get_output_primary_reply +xcb_randr_get_output_primary_unchecked +xcb_randr_get_output_property +xcb_randr_get_output_property_data +xcb_randr_get_output_property_data_end +xcb_randr_get_output_property_data_length +xcb_randr_get_output_property_reply +xcb_randr_get_output_property_unchecked +xcb_randr_get_panning +xcb_randr_get_panning_reply +xcb_randr_get_panning_unchecked +xcb_randr_get_screen_info +xcb_randr_get_screen_info_rates_iterator +xcb_randr_get_screen_info_rates_length +xcb_randr_get_screen_info_reply +xcb_randr_get_screen_info_sizes +xcb_randr_get_screen_info_sizes_iterator +xcb_randr_get_screen_info_sizes_length +xcb_randr_get_screen_info_unchecked +xcb_randr_get_screen_resources +xcb_randr_get_screen_resources_crtcs +xcb_randr_get_screen_resources_crtcs_end +xcb_randr_get_screen_resources_crtcs_length +xcb_randr_get_screen_resources_current +xcb_randr_get_screen_resources_current_crtcs +xcb_randr_get_screen_resources_current_crtcs_end +xcb_randr_get_screen_resources_current_crtcs_length +xcb_randr_get_screen_resources_current_modes +xcb_randr_get_screen_resources_current_modes_iterator +xcb_randr_get_screen_resources_current_modes_length +xcb_randr_get_screen_resources_current_names +xcb_randr_get_screen_resources_current_names_end +xcb_randr_get_screen_resources_current_names_length +xcb_randr_get_screen_resources_current_outputs +xcb_randr_get_screen_resources_current_outputs_end +xcb_randr_get_screen_resources_current_outputs_length +xcb_randr_get_screen_resources_current_reply +xcb_randr_get_screen_resources_current_unchecked +xcb_randr_get_screen_resources_modes +xcb_randr_get_screen_resources_modes_iterator +xcb_randr_get_screen_resources_modes_length +xcb_randr_get_screen_resources_names +xcb_randr_get_screen_resources_names_end +xcb_randr_get_screen_resources_names_length +xcb_randr_get_screen_resources_outputs +xcb_randr_get_screen_resources_outputs_end +xcb_randr_get_screen_resources_outputs_length +xcb_randr_get_screen_resources_reply +xcb_randr_get_screen_resources_unchecked +xcb_randr_get_screen_size_range +xcb_randr_get_screen_size_range_reply +xcb_randr_get_screen_size_range_unchecked +xcb_randr_list_output_properties +xcb_randr_list_output_properties_atoms +xcb_randr_list_output_properties_atoms_end +xcb_randr_list_output_properties_atoms_length +xcb_randr_list_output_properties_reply +xcb_randr_list_output_properties_unchecked +xcb_randr_notify_event_t +xcb_randr_query_output_property +xcb_randr_query_output_property_reply +xcb_randr_query_output_property_unchecked +xcb_randr_query_output_property_valid_values +xcb_randr_query_output_property_valid_values_end +xcb_randr_query_output_property_valid_values_length +xcb_randr_query_version +xcb_randr_query_version_reply +xcb_randr_query_version_unchecked +xcb_randr_screen_change_notify_event_t +xcb_randr_select_input +xcb_randr_select_input_checked +xcb_randr_set_crtc_config +xcb_randr_set_crtc_config_reply +xcb_randr_set_crtc_config_unchecked +xcb_randr_set_crtc_gamma +xcb_randr_set_crtc_gamma_checked +xcb_randr_set_crtc_transform +xcb_randr_set_crtc_transform_checked +xcb_randr_set_output_primary +xcb_randr_set_output_primary_checked +xcb_randr_set_panning +xcb_randr_set_panning_reply +xcb_randr_set_panning_unchecked +xcb_randr_set_screen_config +xcb_randr_set_screen_config_reply +xcb_randr_set_screen_config_unchecked +xcb_randr_set_screen_size +xcb_randr_set_screen_size_checked +xcb_recolor_cursor +xcb_recolor_cursor_checked +xcb_record_create_context +xcb_record_create_context_checked +xcb_record_disable_context +xcb_record_disable_context_checked +xcb_record_enable_context +xcb_record_enable_context_data +xcb_record_enable_context_data_end +xcb_record_enable_context_data_length +xcb_record_enable_context_reply +xcb_record_enable_context_unchecked +xcb_record_free_context +xcb_record_free_context_checked +xcb_record_get_context +xcb_record_get_context_intercepted_clients_iterator +xcb_record_get_context_intercepted_clients_length +xcb_record_get_context_reply +xcb_record_get_context_unchecked +xcb_record_query_version +xcb_record_query_version_reply +xcb_record_query_version_unchecked +xcb_record_register_clients +xcb_record_register_clients_checked +xcb_record_unregister_clients +xcb_record_unregister_clients_checked +xcb_render_add_glyphs +xcb_render_add_glyphs_checked +xcb_render_add_traps +xcb_render_add_traps_checked +xcb_render_change_picture +xcb_render_change_picture_checked +xcb_render_composite +xcb_render_composite_checked +xcb_render_composite_glyphs_16 +xcb_render_composite_glyphs_16_checked +xcb_render_composite_glyphs_32 +xcb_render_composite_glyphs_32_checked +xcb_render_composite_glyphs_8 +xcb_render_composite_glyphs_8_checked +xcb_render_create_anim_cursor +xcb_render_create_anim_cursor_checked +xcb_render_create_conical_gradient +xcb_render_create_conical_gradient_checked +xcb_render_create_cursor +xcb_render_create_cursor_checked +xcb_render_create_glyph_set +xcb_render_create_glyph_set_checked +xcb_render_create_linear_gradient +xcb_render_create_linear_gradient_checked +xcb_render_create_picture +xcb_render_create_picture_checked +xcb_render_create_radial_gradient +xcb_render_create_radial_gradient_checked +xcb_render_create_solid_fill +xcb_render_create_solid_fill_checked +xcb_render_fill_rectangles +xcb_render_fill_rectangles_checked +xcb_render_free_glyphs +xcb_render_free_glyphs_checked +xcb_render_free_glyph_set +xcb_render_free_glyph_set_checked +xcb_render_free_picture +xcb_render_free_picture_checked +xcb_render_query_filters +xcb_render_query_filters_aliases +xcb_render_query_filters_aliases_end +xcb_render_query_filters_aliases_length +xcb_render_query_filters_filters_iterator +xcb_render_query_filters_filters_length +xcb_render_query_filters_reply +xcb_render_query_filters_unchecked +xcb_render_query_pict_formats +xcb_render_query_pict_formats_formats +xcb_render_query_pict_formats_formats_iterator +xcb_render_query_pict_formats_formats_length +xcb_render_query_pict_formats_reply +xcb_render_query_pict_formats_screens_iterator +xcb_render_query_pict_formats_screens_length +xcb_render_query_pict_formats_subpixels +xcb_render_query_pict_formats_subpixels_end +xcb_render_query_pict_formats_subpixels_length +xcb_render_query_pict_formats_unchecked +xcb_render_query_pict_index_values +xcb_render_query_pict_index_values_reply +xcb_render_query_pict_index_values_unchecked +xcb_render_query_pict_index_values_values +xcb_render_query_pict_index_values_values_iterator +xcb_render_query_pict_index_values_values_length +xcb_render_query_version +xcb_render_query_version_reply +xcb_render_query_version_unchecked +xcb_render_reference_glyph_set +xcb_render_reference_glyph_set_checked +xcb_render_set_picture_clip_rectangles +xcb_render_set_picture_clip_rectangles_checked +xcb_render_set_picture_filter +xcb_render_set_picture_filter_checked +xcb_render_set_picture_transform +xcb_render_set_picture_transform_checked +xcb_render_trapezoids +xcb_render_trapezoids_checked +xcb_render_triangles +xcb_render_triangles_checked +xcb_render_tri_fan +xcb_render_tri_fan_checked +xcb_render_tri_strip +xcb_render_tri_strip_checked +xcb_reparent_notify_event_t +xcb_reparent_window +xcb_reparent_window_checked +xcb_resize_request_event_t +xcb_res_query_client_pixmap_bytes +xcb_res_query_client_pixmap_bytes_reply +xcb_res_query_client_pixmap_bytes_unchecked +xcb_res_query_client_resources +xcb_res_query_client_resources_reply +xcb_res_query_client_resources_types +xcb_res_query_client_resources_types_iterator +xcb_res_query_client_resources_types_length +xcb_res_query_client_resources_unchecked +xcb_res_query_clients +xcb_res_query_clients_clients +xcb_res_query_clients_clients_iterator +xcb_res_query_clients_clients_length +xcb_res_query_clients_reply +xcb_res_query_clients_unchecked +xcb_res_query_version +xcb_res_query_version_reply +xcb_res_query_version_unchecked +xcb_rotate_properties +xcb_rotate_properties_checked +xcb_screensaver_notify_event_t +xcb_screensaver_query_info +xcb_screensaver_query_info_reply +xcb_screensaver_query_info_unchecked +xcb_screensaver_query_version +xcb_screensaver_query_version_reply +xcb_screensaver_query_version_unchecked +xcb_screensaver_select_input +xcb_screensaver_select_input_checked +xcb_screensaver_set_attributes +xcb_screensaver_set_attributes_checked +xcb_screensaver_suspend +xcb_screensaver_suspend_checked +xcb_screensaver_unset_attributes +xcb_screensaver_unset_attributes_checked +xcb_selection_clear_event_t +xcb_selection_notify_event_t +xcb_selection_request_event_t +xcb_selinux_get_client_context +xcb_selinux_get_client_context_context +xcb_selinux_get_client_context_context_end +xcb_selinux_get_client_context_context_length +xcb_selinux_get_client_context_reply +xcb_selinux_get_client_context_unchecked +xcb_selinux_get_device_context +xcb_selinux_get_device_context_context +xcb_selinux_get_device_context_context_end +xcb_selinux_get_device_context_context_length +xcb_selinux_get_device_context_reply +xcb_selinux_get_device_context_unchecked +xcb_selinux_get_device_create_context +xcb_selinux_get_device_create_context_context +xcb_selinux_get_device_create_context_context_end +xcb_selinux_get_device_create_context_context_length +xcb_selinux_get_device_create_context_reply +xcb_selinux_get_device_create_context_unchecked +xcb_selinux_get_property_context +xcb_selinux_get_property_context_context +xcb_selinux_get_property_context_context_end +xcb_selinux_get_property_context_context_length +xcb_selinux_get_property_context_reply +xcb_selinux_get_property_context_unchecked +xcb_selinux_get_property_create_context +xcb_selinux_get_property_create_context_context +xcb_selinux_get_property_create_context_context_end +xcb_selinux_get_property_create_context_context_length +xcb_selinux_get_property_create_context_reply +xcb_selinux_get_property_create_context_unchecked +xcb_selinux_get_property_data_context +xcb_selinux_get_property_data_context_context +xcb_selinux_get_property_data_context_context_end +xcb_selinux_get_property_data_context_context_length +xcb_selinux_get_property_data_context_reply +xcb_selinux_get_property_data_context_unchecked +xcb_selinux_get_property_use_context +xcb_selinux_get_property_use_context_context +xcb_selinux_get_property_use_context_context_end +xcb_selinux_get_property_use_context_context_length +xcb_selinux_get_property_use_context_reply +xcb_selinux_get_property_use_context_unchecked +xcb_selinux_get_selection_context +xcb_selinux_get_selection_context_context +xcb_selinux_get_selection_context_context_end +xcb_selinux_get_selection_context_context_length +xcb_selinux_get_selection_context_reply +xcb_selinux_get_selection_context_unchecked +xcb_selinux_get_selection_create_context +xcb_selinux_get_selection_create_context_context +xcb_selinux_get_selection_create_context_context_end +xcb_selinux_get_selection_create_context_context_length +xcb_selinux_get_selection_create_context_reply +xcb_selinux_get_selection_create_context_unchecked +xcb_selinux_get_selection_data_context +xcb_selinux_get_selection_data_context_context +xcb_selinux_get_selection_data_context_context_end +xcb_selinux_get_selection_data_context_context_length +xcb_selinux_get_selection_data_context_reply +xcb_selinux_get_selection_data_context_unchecked +xcb_selinux_get_selection_use_context +xcb_selinux_get_selection_use_context_context +xcb_selinux_get_selection_use_context_context_end +xcb_selinux_get_selection_use_context_context_length +xcb_selinux_get_selection_use_context_reply +xcb_selinux_get_selection_use_context_unchecked +xcb_selinux_get_window_context +xcb_selinux_get_window_context_context +xcb_selinux_get_window_context_context_end +xcb_selinux_get_window_context_context_length +xcb_selinux_get_window_context_reply +xcb_selinux_get_window_context_unchecked +xcb_selinux_get_window_create_context +xcb_selinux_get_window_create_context_context +xcb_selinux_get_window_create_context_context_end +xcb_selinux_get_window_create_context_context_length +xcb_selinux_get_window_create_context_reply +xcb_selinux_get_window_create_context_unchecked +xcb_selinux_list_properties +xcb_selinux_list_properties_properties_iterator +xcb_selinux_list_properties_properties_length +xcb_selinux_list_properties_reply +xcb_selinux_list_properties_unchecked +xcb_selinux_list_selections +xcb_selinux_list_selections_reply +xcb_selinux_list_selections_selections_iterator +xcb_selinux_list_selections_selections_length +xcb_selinux_list_selections_unchecked +xcb_selinux_query_version +xcb_selinux_query_version_reply +xcb_selinux_query_version_unchecked +xcb_selinux_set_device_context +xcb_selinux_set_device_context_checked +xcb_selinux_set_device_create_context +xcb_selinux_set_device_create_context_checked +xcb_selinux_set_property_create_context +xcb_selinux_set_property_create_context_checked +xcb_selinux_set_property_use_context +xcb_selinux_set_property_use_context_checked +xcb_selinux_set_selection_create_context +xcb_selinux_set_selection_create_context_checked +xcb_selinux_set_selection_use_context +xcb_selinux_set_selection_use_context_checked +xcb_selinux_set_window_create_context +xcb_selinux_set_window_create_context_checked +xcb_send_event +xcb_send_event_checked +xcb_set_access_control +xcb_set_access_control_checked +xcb_set_clip_rectangles +xcb_set_clip_rectangles_checked +xcb_set_close_down_mode +xcb_set_close_down_mode_checked +xcb_set_dashes +xcb_set_dashes_checked +xcb_set_font_path +xcb_set_font_path_checked +xcb_set_input_focus +xcb_set_input_focus_checked +xcb_set_modifier_mapping +xcb_set_modifier_mapping_reply +xcb_set_modifier_mapping_unchecked +xcb_set_pointer_mapping +xcb_set_pointer_mapping_reply +xcb_set_pointer_mapping_unchecked +xcb_set_screen_saver +xcb_set_screen_saver_checked +xcb_set_selection_owner +xcb_set_selection_owner_checked +xcb_shape_combine +xcb_shape_combine_checked +xcb_shape_get_rectangles +xcb_shape_get_rectangles_rectangles +xcb_shape_get_rectangles_rectangles_iterator +xcb_shape_get_rectangles_rectangles_length +xcb_shape_get_rectangles_reply +xcb_shape_get_rectangles_unchecked +xcb_shape_input_selected +xcb_shape_input_selected_reply +xcb_shape_input_selected_unchecked +xcb_shape_mask +xcb_shape_mask_checked +xcb_shape_notify_event_t +xcb_shape_offset +xcb_shape_offset_checked +xcb_shape_query_extents +xcb_shape_query_extents_reply +xcb_shape_query_extents_unchecked +xcb_shape_query_version +xcb_shape_query_version_reply +xcb_shape_query_version_unchecked +xcb_shape_rectangles +xcb_shape_rectangles_checked +xcb_shape_select_input +xcb_shape_select_input_checked +xcb_shm_attach +xcb_shm_attach_checked +xcb_shm_completion_event_t +xcb_shm_create_pixmap +xcb_shm_create_pixmap_checked +xcb_shm_detach +xcb_shm_detach_checked +xcb_shm_get_image +xcb_shm_get_image_reply +xcb_shm_get_image_unchecked +xcb_shm_put_image +xcb_shm_put_image_checked +xcb_shm_query_version +xcb_shm_query_version_reply +xcb_shm_query_version_unchecked +xcb_store_colors +xcb_store_colors_checked +xcb_store_named_color +xcb_store_named_color_checked +xcb_sync_alarm_notify_event_t +xcb_sync_await +xcb_sync_await_checked +xcb_sync_await_fence +xcb_sync_await_fence_checked +xcb_sync_change_alarm +xcb_sync_change_alarm_checked +xcb_sync_change_counter +xcb_sync_change_counter_checked +xcb_sync_counter_notify_event_t +xcb_sync_create_alarm +xcb_sync_create_alarm_checked +xcb_sync_create_counter +xcb_sync_create_counter_checked +xcb_sync_create_fence +xcb_sync_create_fence_checked +xcb_sync_destroy_alarm +xcb_sync_destroy_alarm_checked +xcb_sync_destroy_counter +xcb_sync_destroy_counter_checked +xcb_sync_destroy_fence +xcb_sync_destroy_fence_checked +xcb_sync_get_priority +xcb_sync_get_priority_reply +xcb_sync_get_priority_unchecked +xcb_sync_initialize +xcb_sync_initialize_reply +xcb_sync_initialize_unchecked +xcb_sync_list_system_counters +xcb_sync_list_system_counters_counters_iterator +xcb_sync_list_system_counters_counters_length +xcb_sync_list_system_counters_reply +xcb_sync_list_system_counters_unchecked +xcb_sync_query_alarm +xcb_sync_query_alarm_reply +xcb_sync_query_alarm_unchecked +xcb_sync_query_counter +xcb_sync_query_counter_reply +xcb_sync_query_counter_unchecked +xcb_sync_query_fence +xcb_sync_query_fence_reply +xcb_sync_query_fence_unchecked +xcb_sync_reset_fence +xcb_sync_reset_fence_checked +xcb_sync_set_counter +xcb_sync_set_counter_checked +xcb_sync_set_priority +xcb_sync_set_priority_checked +xcb_sync_trigger_fence +xcb_sync_trigger_fence_checked +xcb_test_compare_cursor +xcb_test_compare_cursor_reply +xcb_test_compare_cursor_unchecked +xcb_test_fake_input +xcb_test_fake_input_checked +xcb_test_get_version +xcb_test_get_version_reply +xcb_test_get_version_unchecked +xcb_test_grab_control +xcb_test_grab_control_checked +xcb_translate_coordinates +xcb_translate_coordinates_reply +xcb_translate_coordinates_unchecked +xcb_ungrab_button +xcb_ungrab_button_checked +xcb_ungrab_key +xcb_ungrab_keyboard +xcb_ungrab_keyboard_checked +xcb_ungrab_key_checked +xcb_ungrab_pointer +xcb_ungrab_pointer_checked +xcb_ungrab_server +xcb_ungrab_server_checked +xcb_uninstall_colormap +xcb_uninstall_colormap_checked +xcb_unmap_notify_event_t +xcb_unmap_subwindows +xcb_unmap_subwindows_checked +xcb_unmap_window +xcb_unmap_window_checked +xcb_visibility_notify_event_t +xcb_warp_pointer +xcb_warp_pointer_checked +xcb_xc_misc_get_version +xcb_xc_misc_get_version_reply +xcb_xc_misc_get_version_unchecked +xcb_xc_misc_get_xid_list +xcb_xc_misc_get_xid_list_ids +xcb_xc_misc_get_xid_list_ids_end +xcb_xc_misc_get_xid_list_ids_length +xcb_xc_misc_get_xid_list_reply +xcb_xc_misc_get_xid_list_unchecked +xcb_xc_misc_get_xid_range +xcb_xc_misc_get_xid_range_reply +xcb_xc_misc_get_xid_range_unchecked +xcb_xevie_end +xcb_xevie_end_reply +xcb_xevie_end_unchecked +xcb_xevie_query_version +xcb_xevie_query_version_reply +xcb_xevie_query_version_unchecked +xcb_xevie_select_input +xcb_xevie_select_input_reply +xcb_xevie_select_input_unchecked +xcb_xevie_send +xcb_xevie_send_reply +xcb_xevie_send_unchecked +xcb_xevie_start +xcb_xevie_start_reply +xcb_xevie_start_unchecked +xcb_xf86dri_auth_connection +xcb_xf86dri_auth_connection_reply +xcb_xf86dri_auth_connection_unchecked +xcb_xf86dri_close_connection +xcb_xf86dri_close_connection_checked +xcb_xf86dri_create_context +xcb_xf86dri_create_context_reply +xcb_xf86dri_create_context_unchecked +xcb_xf86dri_create_drawable +xcb_xf86dri_create_drawable_reply +xcb_xf86dri_create_drawable_unchecked +xcb_xf86dri_destroy_context +xcb_xf86dri_destroy_context_checked +xcb_xf86dri_destroy_drawable +xcb_xf86dri_destroy_drawable_checked +xcb_xf86dri_get_client_driver_name +xcb_xf86dri_get_client_driver_name_client_driver_name +xcb_xf86dri_get_client_driver_name_client_driver_name_end +xcb_xf86dri_get_client_driver_name_client_driver_name_length +xcb_xf86dri_get_client_driver_name_reply +xcb_xf86dri_get_client_driver_name_unchecked +xcb_xf86dri_get_device_info +xcb_xf86dri_get_device_info_device_private +xcb_xf86dri_get_device_info_device_private_end +xcb_xf86dri_get_device_info_device_private_length +xcb_xf86dri_get_device_info_reply +xcb_xf86dri_get_device_info_unchecked +xcb_xf86dri_get_drawable_info +xcb_xf86dri_get_drawable_info_back_clip_rects +xcb_xf86dri_get_drawable_info_back_clip_rects_iterator +xcb_xf86dri_get_drawable_info_back_clip_rects_length +xcb_xf86dri_get_drawable_info_clip_rects +xcb_xf86dri_get_drawable_info_clip_rects_iterator +xcb_xf86dri_get_drawable_info_clip_rects_length +xcb_xf86dri_get_drawable_info_reply +xcb_xf86dri_get_drawable_info_unchecked +xcb_xf86dri_open_connection +xcb_xf86dri_open_connection_bus_id +xcb_xf86dri_open_connection_bus_id_end +xcb_xf86dri_open_connection_bus_id_length +xcb_xf86dri_open_connection_reply +xcb_xf86dri_open_connection_unchecked +xcb_xf86dri_query_direct_rendering_capable +xcb_xf86dri_query_direct_rendering_capable_reply +xcb_xf86dri_query_direct_rendering_capable_unchecked +xcb_xf86dri_query_version +xcb_xf86dri_query_version_reply +xcb_xf86dri_query_version_unchecked +xcb_xfixes_change_cursor +xcb_xfixes_change_cursor_by_name +xcb_xfixes_change_cursor_by_name_checked +xcb_xfixes_change_cursor_checked +xcb_xfixes_change_save_set +xcb_xfixes_change_save_set_checked +xcb_xfixes_copy_region +xcb_xfixes_copy_region_checked +xcb_xfixes_create_region +xcb_xfixes_create_region_checked +xcb_xfixes_create_region_from_bitmap +xcb_xfixes_create_region_from_bitmap_checked +xcb_xfixes_create_region_from_gc +xcb_xfixes_create_region_from_gc_checked +xcb_xfixes_create_region_from_picture +xcb_xfixes_create_region_from_picture_checked +xcb_xfixes_create_region_from_window +xcb_xfixes_create_region_from_window_checked +xcb_xfixes_cursor_notify_event_t +xcb_xfixes_destroy_region +xcb_xfixes_destroy_region_checked +xcb_xfixes_expand_region +xcb_xfixes_expand_region_checked +xcb_xfixes_fetch_region +xcb_xfixes_fetch_region_rectangles +xcb_xfixes_fetch_region_rectangles_iterator +xcb_xfixes_fetch_region_rectangles_length +xcb_xfixes_fetch_region_reply +xcb_xfixes_fetch_region_unchecked +xcb_xfixes_get_cursor_image +xcb_xfixes_get_cursor_image_and_name +xcb_xfixes_get_cursor_image_and_name_cursor_image +xcb_xfixes_get_cursor_image_and_name_cursor_image_end +xcb_xfixes_get_cursor_image_and_name_cursor_image_length +xcb_xfixes_get_cursor_image_and_name_name +xcb_xfixes_get_cursor_image_and_name_name_end +xcb_xfixes_get_cursor_image_and_name_name_length +xcb_xfixes_get_cursor_image_and_name_reply +xcb_xfixes_get_cursor_image_and_name_unchecked +xcb_xfixes_get_cursor_image_cursor_image +xcb_xfixes_get_cursor_image_cursor_image_end +xcb_xfixes_get_cursor_image_cursor_image_length +xcb_xfixes_get_cursor_image_reply +xcb_xfixes_get_cursor_image_unchecked +xcb_xfixes_get_cursor_name +xcb_xfixes_get_cursor_name_name +xcb_xfixes_get_cursor_name_name_end +xcb_xfixes_get_cursor_name_name_length +xcb_xfixes_get_cursor_name_reply +xcb_xfixes_get_cursor_name_unchecked +xcb_xfixes_hide_cursor +xcb_xfixes_hide_cursor_checked +xcb_xfixes_intersect_region +xcb_xfixes_intersect_region_checked +xcb_xfixes_invert_region +xcb_xfixes_invert_region_checked +xcb_xfixes_query_version +xcb_xfixes_query_version_reply +xcb_xfixes_query_version_unchecked +xcb_xfixes_region_extents +xcb_xfixes_region_extents_checked +xcb_xfixes_select_cursor_input +xcb_xfixes_select_cursor_input_checked +xcb_xfixes_selection_notify_event_t +xcb_xfixes_select_selection_input +xcb_xfixes_select_selection_input_checked +xcb_xfixes_set_cursor_name +xcb_xfixes_set_cursor_name_checked +xcb_xfixes_set_gc_clip_region +xcb_xfixes_set_gc_clip_region_checked +xcb_xfixes_set_picture_clip_region +xcb_xfixes_set_picture_clip_region_checked +xcb_xfixes_set_region +xcb_xfixes_set_region_checked +xcb_xfixes_set_window_shape_region +xcb_xfixes_set_window_shape_region_checked +xcb_xfixes_show_cursor +xcb_xfixes_show_cursor_checked +xcb_xfixes_subtract_region +xcb_xfixes_subtract_region_checked +xcb_xfixes_translate_region +xcb_xfixes_translate_region_checked +xcb_xfixes_union_region +xcb_xfixes_union_region_checked +xcb_xinerama_get_screen_count +xcb_xinerama_get_screen_count_reply +xcb_xinerama_get_screen_count_unchecked +xcb_xinerama_get_screen_size +xcb_xinerama_get_screen_size_reply +xcb_xinerama_get_screen_size_unchecked +xcb_xinerama_get_state +xcb_xinerama_get_state_reply +xcb_xinerama_get_state_unchecked +xcb_xinerama_is_active +xcb_xinerama_is_active_reply +xcb_xinerama_is_active_unchecked +xcb_xinerama_query_screens +xcb_xinerama_query_screens_reply +xcb_xinerama_query_screens_screen_info +xcb_xinerama_query_screens_screen_info_iterator +xcb_xinerama_query_screens_screen_info_length +xcb_xinerama_query_screens_unchecked +xcb_xinerama_query_version +xcb_xinerama_query_version_reply +xcb_xinerama_query_version_unchecked +xcb_xkb_access_x_notify_event_t +xcb_xkb_action_message_event_t +xcb_xkb_bell +xcb_xkb_bell_checked +xcb_xkb_bell_notify_event_t +xcb_xkb_compat_map_notify_event_t +xcb_xkb_controls_notify_event_t +xcb_xkb_extension_device_notify_event_t +xcb_xkb_get_compat_map +xcb_xkb_get_compat_map_group_rtrn +xcb_xkb_get_compat_map_group_rtrn_iterator +xcb_xkb_get_compat_map_group_rtrn_length +xcb_xkb_get_compat_map_reply +xcb_xkb_get_compat_map_si_rtrn +xcb_xkb_get_compat_map_si_rtrn_end +xcb_xkb_get_compat_map_si_rtrn_length +xcb_xkb_get_compat_map_unchecked +xcb_xkb_get_controls +xcb_xkb_get_controls_reply +xcb_xkb_get_controls_unchecked +xcb_xkb_get_device_info +xcb_xkb_get_device_info_btn_actions +xcb_xkb_get_device_info_btn_actions_iterator +xcb_xkb_get_device_info_btn_actions_length +xcb_xkb_get_device_info_leds_iterator +xcb_xkb_get_device_info_leds_length +xcb_xkb_get_device_info_name +xcb_xkb_get_device_info_name_end +xcb_xkb_get_device_info_name_length +xcb_xkb_get_device_info_reply +xcb_xkb_get_device_info_unchecked +xcb_xkb_get_geometry +xcb_xkb_get_geometry_colors_iterator +xcb_xkb_get_geometry_colors_length +xcb_xkb_get_geometry_doodads_iterator +xcb_xkb_get_geometry_doodads_length +xcb_xkb_get_geometry_key_aliases +xcb_xkb_get_geometry_key_aliases_iterator +xcb_xkb_get_geometry_key_aliases_length +xcb_xkb_get_geometry_label_font +xcb_xkb_get_geometry_properties_iterator +xcb_xkb_get_geometry_properties_length +xcb_xkb_get_geometry_reply +xcb_xkb_get_geometry_sections_iterator +xcb_xkb_get_geometry_sections_length +xcb_xkb_get_geometry_shapes_iterator +xcb_xkb_get_geometry_shapes_length +xcb_xkb_get_geometry_unchecked +xcb_xkb_get_indicator_map +xcb_xkb_get_indicator_map_maps +xcb_xkb_get_indicator_map_maps_iterator +xcb_xkb_get_indicator_map_maps_length +xcb_xkb_get_indicator_map_reply +xcb_xkb_get_indicator_map_unchecked +xcb_xkb_get_indicator_state +xcb_xkb_get_indicator_state_reply +xcb_xkb_get_indicator_state_unchecked +xcb_xkb_get_kbd_by_name +xcb_xkb_get_kbd_by_name_replies +xcb_xkb_get_kbd_by_name_reply +xcb_xkb_get_kbd_by_name_unchecked +xcb_xkb_get_map +xcb_xkb_get_map_map +xcb_xkb_get_map_reply +xcb_xkb_get_map_unchecked +xcb_xkb_get_named_indicator +xcb_xkb_get_named_indicator_reply +xcb_xkb_get_named_indicator_unchecked +xcb_xkb_get_names +xcb_xkb_get_names_reply +xcb_xkb_get_names_unchecked +xcb_xkb_get_names_value_list +xcb_xkb_get_state +xcb_xkb_get_state_reply +xcb_xkb_get_state_unchecked +xcb_xkb_indicator_map_notify_event_t +xcb_xkb_indicator_state_notify_event_t +xcb_xkb_latch_lock_state +xcb_xkb_latch_lock_state_checked +xcb_xkb_list_components +xcb_xkb_list_components_compat_maps_iterator +xcb_xkb_list_components_compat_maps_length +xcb_xkb_list_components_geometries_iterator +xcb_xkb_list_components_geometries_length +xcb_xkb_list_components_keycodes_iterator +xcb_xkb_list_components_keycodes_length +xcb_xkb_list_components_keymaps_iterator +xcb_xkb_list_components_keymaps_length +xcb_xkb_list_components_reply +xcb_xkb_list_components_symbols_iterator +xcb_xkb_list_components_symbols_length +xcb_xkb_list_components_types_iterator +xcb_xkb_list_components_types_length +xcb_xkb_list_components_unchecked +xcb_xkb_map_notify_event_t +xcb_xkb_names_notify_event_t +xcb_xkb_new_keyboard_notify_event_t +xcb_xkb_per_client_flags +xcb_xkb_per_client_flags_reply +xcb_xkb_per_client_flags_unchecked +xcb_xkb_select_events +xcb_xkb_select_events_checked +xcb_xkb_set_compat_map +xcb_xkb_set_compat_map_checked +xcb_xkb_set_controls +xcb_xkb_set_controls_checked +xcb_xkb_set_debugging_flags +xcb_xkb_set_debugging_flags_reply +xcb_xkb_set_debugging_flags_unchecked +xcb_xkb_set_device_info +xcb_xkb_set_device_info_checked +xcb_xkb_set_geometry +xcb_xkb_set_geometry_checked +xcb_xkb_set_indicator_map +xcb_xkb_set_indicator_map_checked +xcb_xkb_set_map +xcb_xkb_set_map_checked +xcb_xkb_set_named_indicator +xcb_xkb_set_named_indicator_checked +xcb_xkb_set_names +xcb_xkb_set_names_checked +xcb_xkb_state_notify_event_t +xcb_xkb_use_extension +xcb_xkb_use_extension_reply +xcb_xkb_use_extension_unchecked +xcb_x_print_attribut_notify_event_t +xcb_x_print_create_context +xcb_x_print_create_context_checked +xcb_x_print_notify_event_t +xcb_x_print_print_destroy_context +xcb_x_print_print_destroy_context_checked +xcb_x_print_print_end_doc +xcb_x_print_print_end_doc_checked +xcb_x_print_print_end_job +xcb_x_print_print_end_job_checked +xcb_x_print_print_end_page +xcb_x_print_print_end_page_checked +xcb_x_print_print_get_attributes +xcb_x_print_print_get_attributes_reply +xcb_x_print_print_get_attributes_unchecked +xcb_x_print_print_get_context +xcb_x_print_print_get_context_reply +xcb_x_print_print_get_context_unchecked +xcb_x_print_print_get_document_data +xcb_x_print_print_get_document_data_data +xcb_x_print_print_get_document_data_data_end +xcb_x_print_print_get_document_data_data_length +xcb_x_print_print_get_document_data_reply +xcb_x_print_print_get_document_data_unchecked +xcb_x_print_print_get_image_resolution +xcb_x_print_print_get_image_resolution_reply +xcb_x_print_print_get_image_resolution_unchecked +xcb_x_print_print_get_one_attributes +xcb_x_print_print_get_one_attributes_reply +xcb_x_print_print_get_one_attributes_unchecked +xcb_x_print_print_get_one_attributes_value +xcb_x_print_print_get_one_attributes_value_end +xcb_x_print_print_get_one_attributes_value_length +xcb_x_print_print_get_page_dimensions +xcb_x_print_print_get_page_dimensions_reply +xcb_x_print_print_get_page_dimensions_unchecked +xcb_x_print_print_get_printer_list +xcb_x_print_print_get_printer_list_printers_iterator +xcb_x_print_print_get_printer_list_printers_length +xcb_x_print_print_get_printer_list_reply +xcb_x_print_print_get_printer_list_unchecked +xcb_x_print_print_get_screen_of_context +xcb_x_print_print_get_screen_of_context_reply +xcb_x_print_print_get_screen_of_context_unchecked +xcb_x_print_print_input_selected +xcb_x_print_print_input_selected_all_events_list +xcb_x_print_print_input_selected_all_events_list_end +xcb_x_print_print_input_selected_all_events_list_length +xcb_x_print_print_input_selected_event_list +xcb_x_print_print_input_selected_event_list_end +xcb_x_print_print_input_selected_event_list_length +xcb_x_print_print_input_selected_reply +xcb_x_print_print_input_selected_unchecked +xcb_x_print_print_put_document_data +xcb_x_print_print_put_document_data_checked +xcb_x_print_print_query_screens +xcb_x_print_print_query_screens_reply +xcb_x_print_print_query_screens_roots +xcb_x_print_print_query_screens_roots_end +xcb_x_print_print_query_screens_roots_length +xcb_x_print_print_query_screens_unchecked +xcb_x_print_print_query_version +xcb_x_print_print_query_version_reply +xcb_x_print_print_query_version_unchecked +xcb_x_print_print_rehash_printer_list +xcb_x_print_print_rehash_printer_list_checked +xcb_x_print_print_select_input +xcb_x_print_print_select_input_checked +xcb_x_print_print_set_attributes +xcb_x_print_print_set_attributes_checked +xcb_x_print_print_set_context +xcb_x_print_print_set_context_checked +xcb_x_print_print_set_image_resolution +xcb_x_print_print_set_image_resolution_reply +xcb_x_print_print_set_image_resolution_unchecked +xcb_x_print_print_start_doc +xcb_x_print_print_start_doc_checked +xcb_x_print_print_start_job +xcb_x_print_print_start_job_checked +xcb_x_print_print_start_page +xcb_x_print_print_start_page_checked +xcb_xv_get_port_attribute +xcb_xv_get_port_attribute_reply +xcb_xv_get_port_attribute_unchecked +xcb_xv_get_still +xcb_xv_get_still_checked +xcb_xv_get_video +xcb_xv_get_video_checked +xcb_xv_grab_port +xcb_xv_grab_port_reply +xcb_xv_grab_port_unchecked +xcb_xv_list_image_formats +xcb_xv_list_image_formats_format +xcb_xv_list_image_formats_format_iterator +xcb_xv_list_image_formats_format_length +xcb_xv_list_image_formats_reply +xcb_xv_list_image_formats_unchecked +xcb_xvmc_create_context +xcb_xvmc_create_context_priv_data +xcb_xvmc_create_context_priv_data_end +xcb_xvmc_create_context_priv_data_length +xcb_xvmc_create_context_reply +xcb_xvmc_create_context_unchecked +xcb_xvmc_create_subpicture +xcb_xvmc_create_subpicture_priv_data +xcb_xvmc_create_subpicture_priv_data_end +xcb_xvmc_create_subpicture_priv_data_length +xcb_xvmc_create_subpicture_reply +xcb_xvmc_create_subpicture_unchecked +xcb_xvmc_create_surface +xcb_xvmc_create_surface_priv_data +xcb_xvmc_create_surface_priv_data_end +xcb_xvmc_create_surface_priv_data_length +xcb_xvmc_create_surface_reply +xcb_xvmc_create_surface_unchecked +xcb_xvmc_destroy_context +xcb_xvmc_destroy_context_checked +xcb_xvmc_destroy_subpicture +xcb_xvmc_destroy_subpicture_checked +xcb_xvmc_destroy_surface +xcb_xvmc_destroy_surface_checked +xcb_xvmc_list_subpicture_types +xcb_xvmc_list_subpicture_types_reply +xcb_xvmc_list_subpicture_types_types +xcb_xvmc_list_subpicture_types_types_iterator +xcb_xvmc_list_subpicture_types_types_length +xcb_xvmc_list_subpicture_types_unchecked +xcb_xvmc_list_surface_types +xcb_xvmc_list_surface_types_reply +xcb_xvmc_list_surface_types_surfaces +xcb_xvmc_list_surface_types_surfaces_iterator +xcb_xvmc_list_surface_types_surfaces_length +xcb_xvmc_list_surface_types_unchecked +xcb_xvmc_query_version +xcb_xvmc_query_version_reply +xcb_xvmc_query_version_unchecked +xcb_xv_port_notify_event_t +xcb_xv_put_image +xcb_xv_put_image_checked +xcb_xv_put_still +xcb_xv_put_still_checked +xcb_xv_put_video +xcb_xv_put_video_checked +xcb_xv_query_adaptors +xcb_xv_query_adaptors_info_iterator +xcb_xv_query_adaptors_info_length +xcb_xv_query_adaptors_reply +xcb_xv_query_adaptors_unchecked +xcb_xv_query_best_size +xcb_xv_query_best_size_reply +xcb_xv_query_best_size_unchecked +xcb_xv_query_encodings +xcb_xv_query_encodings_info_iterator +xcb_xv_query_encodings_info_length +xcb_xv_query_encodings_reply +xcb_xv_query_encodings_unchecked +xcb_xv_query_extension +xcb_xv_query_extension_reply +xcb_xv_query_extension_unchecked +xcb_xv_query_image_attributes +xcb_xv_query_image_attributes_offsets +xcb_xv_query_image_attributes_offsets_end +xcb_xv_query_image_attributes_offsets_length +xcb_xv_query_image_attributes_pitches +xcb_xv_query_image_attributes_pitches_end +xcb_xv_query_image_attributes_pitches_length +xcb_xv_query_image_attributes_reply +xcb_xv_query_image_attributes_unchecked +xcb_xv_query_port_attributes +xcb_xv_query_port_attributes_attributes_iterator +xcb_xv_query_port_attributes_attributes_length +xcb_xv_query_port_attributes_reply +xcb_xv_query_port_attributes_unchecked +xcb_xv_select_port_notify +xcb_xv_select_port_notify_checked +xcb_xv_select_video_notify +xcb_xv_select_video_notify_checked +xcb_xv_set_port_attribute +xcb_xv_set_port_attribute_checked +xcb_xv_shm_put_image +xcb_xv_shm_put_image_checked +xcb_xv_stop_video +xcb_xv_stop_video_checked +xcb_xv_ungrab_port +xcb_xv_ungrab_port_checked +xcb_xv_video_notify_event_t +xdr_accepted_reply +xdr_array +xdr_authunix_parms +xdr_bool +xdr_bytes +xdr_callhdr +xdr_callmsg +xdr_char +xdr_destroy +xdr_double +xdr_enum +xdr_float +xdr_free +xdr_getpos +xdr_inline +xdr_int +xdr_long +xdrmem_create +xdr_opaque +xdr_opaque_auth +xdr_pmap +xdr_pmaplist +xdr_pointer +xdrrec_create +xdrrec_endofrecord +xdrrec_eof +xdrrec_skiprecord +xdr_reference +xdr_rejected_reply +xdr_replymsg +xdr_setpos +xdr_short +xdrstdio_create +xdr_string +xdr_u_char +xdr_u_int +xdr_u_long +xdr_union +xdr_u_short +xdr_vector +xdr_void +xdr_wrapstring +xor_mode +xprt_register +xprt_unregister +xwin_set_window_name +soup_add_completion +soup_add_idle +soup_add_io_watch +SoupAddress +SOUP_ADDRESS_ANY_PORT +SoupAddressCallback +soup_address_equal_by_ip +soup_address_equal_by_name +SOUP_ADDRESS_FAMILY +soup_address_get_gsockaddr +soup_address_get_name +soup_address_get_physical +soup_address_get_port +soup_address_get_sockaddr +soup_address_hash_by_ip +soup_address_hash_by_name +soup_address_is_resolved +SOUP_ADDRESS_NAME +soup_address_new +soup_address_new_any +soup_address_new_from_sockaddr +SOUP_ADDRESS_PHYSICAL +SOUP_ADDRESS_PORT +SOUP_ADDRESS_PROTOCOL +soup_address_resolve_async +soup_address_resolve_sync +SOUP_ADDRESS_SOCKADDR +soup_add_timeout +SoupAuth +soup_auth_authenticate +SoupAuthDomain +soup_auth_domain_accepts +soup_auth_domain_add_path +SOUP_AUTH_DOMAIN_ADD_PATH +SoupAuthDomainBasic +SoupAuthDomainBasicAuthCallback +SOUP_AUTH_DOMAIN_BASIC_AUTH_CALLBACK +SOUP_AUTH_DOMAIN_BASIC_AUTH_DATA +soup_auth_domain_basic_new +soup_auth_domain_basic_set_auth_callback +soup_auth_domain_challenge +soup_auth_domain_check_password +soup_auth_domain_covers +SoupAuthDomainDigest +SoupAuthDomainDigestAuthCallback +SOUP_AUTH_DOMAIN_DIGEST_AUTH_CALLBACK +SOUP_AUTH_DOMAIN_DIGEST_AUTH_DATA +soup_auth_domain_digest_encode_password +soup_auth_domain_digest_new +soup_auth_domain_digest_set_auth_callback +SoupAuthDomainFilter +SOUP_AUTH_DOMAIN_FILTER +SOUP_AUTH_DOMAIN_FILTER_DATA +SoupAuthDomainGenericAuthCallback +SOUP_AUTH_DOMAIN_GENERIC_AUTH_CALLBACK +SOUP_AUTH_DOMAIN_GENERIC_AUTH_DATA +soup_auth_domain_get_realm +SOUP_AUTH_DOMAIN_PROXY +SOUP_AUTH_DOMAIN_REALM +soup_auth_domain_remove_path +SOUP_AUTH_DOMAIN_REMOVE_PATH +soup_auth_domain_set_filter +soup_auth_domain_set_generic_auth_callback +soup_auth_free_protection_space +soup_auth_get_authorization +soup_auth_get_host +soup_auth_get_info +soup_auth_get_protection_space +soup_auth_get_realm +soup_auth_get_scheme_name +SOUP_AUTH_HOST +soup_auth_is_authenticated +SOUP_AUTH_IS_AUTHENTICATED +soup_auth_is_for_proxy +SOUP_AUTH_IS_FOR_PROXY +soup_auth_new +SOUP_AUTH_REALM +SOUP_AUTH_SCHEME_NAME +soup_auth_update +SoupBuffer +soup_buffer_copy +soup_buffer_free +soup_buffer_get_as_bytes +soup_buffer_get_data +soup_buffer_get_owner +soup_buffer_new +soup_buffer_new_subbuffer +soup_buffer_new_take +soup_buffer_new_with_owner +SoupCache +soup_cache_clear +soup_cache_dump +soup_cache_flush +soup_cache_get_max_size +soup_cache_load +soup_cache_new +soup_cache_set_max_size +soup_check_version +SOUP_CHECK_VERSION +SoupChunkAllocator +SoupClientContext +soup_client_context_get_address +soup_client_context_get_auth_domain +soup_client_context_get_auth_user +soup_client_context_get_host +soup_client_context_get_socket +SoupContentDecoder +SoupContentSniffer +soup_content_sniffer_get_buffer_size +soup_content_sniffer_new +soup_content_sniffer_sniff +SoupCookie +soup_cookie_applies_to_uri +soup_cookie_copy +soup_cookie_domain_matches +soup_cookie_free +soup_cookie_get_domain +soup_cookie_get_expires +soup_cookie_get_http_only +soup_cookie_get_name +soup_cookie_get_path +soup_cookie_get_secure +soup_cookie_get_value +SoupCookieJar +SOUP_COOKIE_JAR_ACCEPT_POLICY +soup_cookie_jar_add_cookie +soup_cookie_jar_add_cookie_with_first_party +soup_cookie_jar_all_cookies +SoupCookieJarDB +SOUP_COOKIE_JAR_DB_FILENAME +soup_cookie_jar_db_new +soup_cookie_jar_delete_cookie +soup_cookie_jar_get_accept_policy +soup_cookie_jar_get_cookie_list +soup_cookie_jar_get_cookies +soup_cookie_jar_is_persistent +soup_cookie_jar_new +SOUP_COOKIE_JAR_READ_ONLY +soup_cookie_jar_set_accept_policy +soup_cookie_jar_set_cookie +soup_cookie_jar_set_cookie_with_first_party +SoupCookieJarText +SOUP_COOKIE_JAR_TEXT_FILENAME +soup_cookie_jar_text_new +SOUP_COOKIE_MAX_AGE_ONE_DAY +SOUP_COOKIE_MAX_AGE_ONE_HOUR +SOUP_COOKIE_MAX_AGE_ONE_WEEK +SOUP_COOKIE_MAX_AGE_ONE_YEAR +soup_cookie_new +soup_cookie_parse +soup_cookie_set_domain +soup_cookie_set_expires +soup_cookie_set_http_only +soup_cookie_set_max_age +soup_cookie_set_name +soup_cookie_set_path +soup_cookie_set_secure +soup_cookie_set_value +soup_cookies_free +soup_cookies_from_request +soup_cookies_from_response +soup_cookies_to_cookie_header +soup_cookies_to_request +soup_cookies_to_response +soup_cookie_to_cookie_header +soup_cookie_to_set_cookie_header +SoupDate +soup_date_free +soup_date_get_day +soup_date_get_hour +soup_date_get_minute +soup_date_get_month +soup_date_get_offset +soup_date_get_second +soup_date_get_utc +soup_date_get_year +soup_date_is_past +soup_date_new +soup_date_new_from_now +soup_date_new_from_string +soup_date_new_from_time_t +soup_date_to_string +soup_date_to_time_t +soup_date_to_timeval +soup_form_decode +soup_form_decode_multipart +soup_form_encode +soup_form_encode_datalist +soup_form_encode_hash +soup_form_encode_valist +SOUP_FORM_MIME_TYPE_MULTIPART +SOUP_FORM_MIME_TYPE_URLENCODED +soup_form_request_new +soup_form_request_new_from_datalist +soup_form_request_new_from_hash +soup_form_request_new_from_multipart +soup_get_major_version +soup_get_micro_version +soup_get_minor_version +soup_header_contains +soup_header_free_list +soup_header_free_param_list +soup_header_g_string_append_param +soup_header_g_string_append_param_quoted +soup_header_parse_list +soup_header_parse_param_list +soup_header_parse_quality_list +soup_header_parse_semi_param_list +soup_headers_parse +soup_headers_parse_request +soup_headers_parse_response +soup_headers_parse_status_line +SOUP_HTTP_ERROR +SoupLogger +soup_logger_attach +soup_logger_detach +SoupLoggerFilter +soup_logger_new +SoupLoggerPrinter +soup_logger_set_printer +soup_logger_set_request_filter +soup_logger_set_response_filter +SOUP_MAJOR_VERSION +SoupMessage +soup_message_add_header_handler +soup_message_add_status_code_handler +SoupMessageBody +soup_message_body_append +soup_message_body_append_buffer +soup_message_body_append_take +soup_message_body_complete +soup_message_body_flatten +soup_message_body_free +soup_message_body_get_accumulate +soup_message_body_get_chunk +soup_message_body_got_chunk +soup_message_body_new +soup_message_body_set_accumulate +soup_message_body_truncate +soup_message_body_wrote_chunk +soup_message_disable_feature +SOUP_MESSAGE_FIRST_PARTY +SOUP_MESSAGE_FLAGS +soup_message_get_address +soup_message_get_first_party +soup_message_get_flags +soup_message_get_https_status +soup_message_get_http_version +soup_message_get_soup_request +soup_message_get_uri +SoupMessageHeaders +soup_message_headers_append +soup_message_headers_clean_connection_headers +soup_message_headers_clear +soup_message_headers_foreach +SoupMessageHeadersForeachFunc +soup_message_headers_free +soup_message_headers_free_ranges +soup_message_headers_get +soup_message_headers_get_content_disposition +soup_message_headers_get_content_length +soup_message_headers_get_content_range +soup_message_headers_get_content_type +soup_message_headers_get_encoding +soup_message_headers_get_expectations +soup_message_headers_get_list +soup_message_headers_get_one +soup_message_headers_get_ranges +SoupMessageHeadersIter +soup_message_headers_iter_init +soup_message_headers_iter_next +soup_message_headers_new +soup_message_headers_remove +soup_message_headers_replace +soup_message_headers_set_content_disposition +soup_message_headers_set_content_length +soup_message_headers_set_content_range +soup_message_headers_set_content_type +soup_message_headers_set_encoding +soup_message_headers_set_expectations +soup_message_headers_set_range +soup_message_headers_set_ranges +SOUP_MESSAGE_HTTP_VERSION +soup_message_is_keepalive +SOUP_MESSAGE_METHOD +soup_message_new +soup_message_new_from_uri +SOUP_MESSAGE_REASON_PHRASE +SOUP_MESSAGE_REQUEST_BODY +SOUP_MESSAGE_REQUEST_HEADERS +SOUP_MESSAGE_RESPONSE_BODY +SOUP_MESSAGE_RESPONSE_HEADERS +SOUP_MESSAGE_SERVER_SIDE +soup_message_set_chunk_allocator +soup_message_set_first_party +soup_message_set_flags +soup_message_set_http_version +soup_message_set_redirect +soup_message_set_request +soup_message_set_response +soup_message_set_status +soup_message_set_status_full +soup_message_set_uri +SOUP_MESSAGE_STATUS_CODE +SOUP_MESSAGE_TLS_CERTIFICATE +SOUP_MESSAGE_TLS_ERRORS +SOUP_MESSAGE_URI +SOUP_METHOD_CONNECT +SOUP_METHOD_COPY +SOUP_METHOD_DELETE +SOUP_METHOD_GET +SOUP_METHOD_HEAD +SOUP_METHOD_LOCK +SOUP_METHOD_MKCOL +SOUP_METHOD_MOVE +SOUP_METHOD_OPTIONS +SOUP_METHOD_POST +SOUP_METHOD_PROPFIND +SOUP_METHOD_PROPPATCH +SOUP_METHOD_PUT +SOUP_METHOD_TRACE +SOUP_METHOD_UNLOCK +SOUP_MICRO_VERSION +SOUP_MINOR_VERSION +SoupMultipart +soup_multipart_append_form_file +soup_multipart_append_form_string +soup_multipart_append_part +soup_multipart_free +soup_multipart_get_length +soup_multipart_get_part +SoupMultipartInputStream +soup_multipart_input_stream_get_headers +soup_multipart_input_stream_new +soup_multipart_input_stream_next_part +soup_multipart_input_stream_next_part_async +soup_multipart_input_stream_next_part_finish +soup_multipart_new +soup_multipart_new_from_message +soup_multipart_to_message +SoupProxyResolverDefault +SoupProxyURIResolver +SoupProxyURIResolverCallback +soup_proxy_uri_resolver_get_proxy_uri_async +soup_proxy_uri_resolver_get_proxy_uri_sync +SoupRange +SoupRequest +SoupRequestData +SOUP_REQUEST_ERROR +SoupRequestFile +soup_request_file_get_file +soup_request_get_content_length +soup_request_get_content_type +soup_request_get_session +soup_request_get_uri +SoupRequestHTTP +soup_request_http_get_message +soup_request_send +soup_request_send_async +soup_request_send_finish +SOUP_REQUEST_SESSION +SOUP_REQUEST_URI +SoupServer +soup_server_add_auth_domain +soup_server_add_handler +SOUP_SERVER_ASYNC_CONTEXT +SoupServerCallback +soup_server_disconnect +soup_server_get_async_context +soup_server_get_listener +soup_server_get_port +SOUP_SERVER_INTERFACE +soup_server_is_https +soup_server_new +soup_server_pause_message +SOUP_SERVER_PORT +soup_server_quit +SOUP_SERVER_RAW_PATHS +soup_server_remove_auth_domain +soup_server_remove_handler +soup_server_run +soup_server_run_async +SOUP_SERVER_SERVER_HEADER +SOUP_SERVER_SSL_CERT_FILE +SOUP_SERVER_SSL_KEY_FILE +SOUP_SERVER_TLS_CERTIFICATE +soup_server_unpause_message +SoupSession +soup_session_abort +SOUP_SESSION_ACCEPT_LANGUAGE +SOUP_SESSION_ACCEPT_LANGUAGE_AUTO +soup_session_add_feature +SOUP_SESSION_ADD_FEATURE +soup_session_add_feature_by_type +SOUP_SESSION_ADD_FEATURE_BY_TYPE +SoupSessionAsync +SOUP_SESSION_ASYNC_CONTEXT +soup_session_async_new +soup_session_async_new_with_options +SoupSessionCallback +soup_session_cancel_message +SoupSessionFeature +SoupSessionFeatureInterface +soup_session_get_async_context +soup_session_get_feature +soup_session_get_feature_for_message +soup_session_get_features +soup_session_has_feature +SOUP_SESSION_HTTP_ALIASES +SOUP_SESSION_HTTPS_ALIASES +SOUP_SESSION_IDLE_TIMEOUT +SOUP_SESSION_MAX_CONNS +SOUP_SESSION_MAX_CONNS_PER_HOST +soup_session_new +soup_session_new_with_options +soup_session_pause_message +soup_session_prefetch_dns +soup_session_prepare_for_uri +SOUP_SESSION_PROXY_URI +soup_session_queue_message +soup_session_redirect_message +soup_session_remove_feature +soup_session_remove_feature_by_type +SOUP_SESSION_REMOVE_FEATURE_BY_TYPE +soup_session_request +soup_session_request_http +soup_session_request_http_uri +soup_session_request_uri +soup_session_requeue_message +soup_session_send_message +SOUP_SESSION_SSL_CA_FILE +SOUP_SESSION_SSL_STRICT +SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE +SoupSessionSync +soup_session_sync_new +soup_session_sync_new_with_options +SOUP_SESSION_TIMEOUT +SOUP_SESSION_TLS_DATABASE +soup_session_unpause_message +SOUP_SESSION_USE_NTLM +SOUP_SESSION_USER_AGENT +SOUP_SESSION_USE_THREAD_CONTEXT +soup_session_would_redirect +SoupSocket +SOUP_SOCKET_ASYNC_CONTEXT +SoupSocketCallback +soup_socket_connect_async +soup_socket_connect_sync +soup_socket_disconnect +SOUP_SOCKET_FLAG_NONBLOCKING +soup_socket_get_fd +soup_socket_get_local_address +soup_socket_get_remote_address +soup_socket_is_connected +SOUP_SOCKET_IS_SERVER +soup_socket_is_ssl +soup_socket_listen +SOUP_SOCKET_LOCAL_ADDRESS +soup_socket_new +soup_socket_read +soup_socket_read_until +SOUP_SOCKET_REMOTE_ADDRESS +SOUP_SOCKET_SSL_CREDENTIALS +SOUP_SOCKET_SSL_FALLBACK +SOUP_SOCKET_SSL_STRICT +soup_socket_start_proxy_ssl +soup_socket_start_ssl +SOUP_SOCKET_TIMEOUT +SOUP_SOCKET_TLS_CERTIFICATE +SOUP_SOCKET_TLS_ERRORS +SOUP_SOCKET_TRUSTED_CERTIFICATE +SOUP_SOCKET_USE_THREAD_CONTEXT +soup_socket_write +soup_status_get_phrase +SOUP_STATUS_IS_CLIENT_ERROR +SOUP_STATUS_IS_INFORMATIONAL +SOUP_STATUS_IS_REDIRECTION +SOUP_STATUS_IS_SERVER_ERROR +SOUP_STATUS_IS_SUCCESSFUL +SOUP_STATUS_IS_TRANSPORT_ERROR +soup_status_proxify +soup_str_case_equal +soup_str_case_hash +soup_tld_domain_is_public_suffix +SOUP_TLD_ERROR +soup_tld_get_base_domain +SOUP_TYPE_AUTH_BASIC +SOUP_TYPE_AUTH_DIGEST +SOUP_TYPE_AUTH_NTLM +SOUP_TYPE_BYTE_ARRAY +SoupURI +soup_uri_copy +soup_uri_copy_host +soup_uri_decode +soup_uri_encode +soup_uri_equal +soup_uri_free +soup_uri_get_fragment +soup_uri_get_host +soup_uri_get_password +soup_uri_get_path +soup_uri_get_port +soup_uri_get_query +soup_uri_get_scheme +soup_uri_get_user +soup_uri_host_equal +soup_uri_host_hash +SOUP_URI_IS_VALID +soup_uri_new +soup_uri_new_with_base +soup_uri_normalize +SOUP_URI_SCHEME_DATA +SOUP_URI_SCHEME_FILE +SOUP_URI_SCHEME_FTP +SOUP_URI_SCHEME_HTTP +SOUP_URI_SCHEME_HTTPS +SOUP_URI_SCHEME_RESOURCE +soup_uri_set_fragment +soup_uri_set_host +soup_uri_set_password +soup_uri_set_path +soup_uri_set_port +soup_uri_set_query +soup_uri_set_query_from_fields +soup_uri_set_query_from_form +soup_uri_set_scheme +soup_uri_set_user +soup_uri_to_string +soup_uri_uses_default_port +SOUP_URI_VALID_FOR_HTTP +soup_value_array_append +soup_value_array_append_vals +soup_value_array_from_args +soup_value_array_get_nth +soup_value_array_insert +soup_value_array_new +soup_value_array_new_with_vals +soup_value_array_to_args +SOUP_VALUE_GETV +soup_value_hash_insert +soup_value_hash_insert_vals +soup_value_hash_insert_value +soup_value_hash_lookup +soup_value_hash_lookup_vals +soup_value_hash_new +soup_value_hash_new_with_vals +SOUP_VALUE_SETV +SOUP_VERSION_2_24 +SOUP_VERSION_2_26 +SOUP_VERSION_2_28 +SOUP_VERSION_2_30 +SOUP_VERSION_2_32 +SOUP_VERSION_2_34 +SOUP_VERSION_2_36 +SOUP_VERSION_2_38 +SOUP_VERSION_2_40 +SOUP_VERSION_2_42 +SOUP_VERSION_MAX_ALLOWED +SOUP_VERSION_MIN_REQUIRED +soup_xmlrpc_build_fault +soup_xmlrpc_build_method_call +soup_xmlrpc_build_method_response +soup_xmlrpc_extract_method_call +soup_xmlrpc_extract_method_response +SOUP_XMLRPC_FAULT +soup_xmlrpc_parse_method_call +soup_xmlrpc_parse_method_response +soup_xmlrpc_request_new +soup_xmlrpc_set_fault +soup_xmlrpc_set_response diff --git a/tests/data/test-multi-paned.ui b/tests/data/test-multi-paned.ui new file mode 100644 index 0000000..ed7f7fa --- /dev/null +++ b/tests/data/test-multi-paned.ui @@ -0,0 +1,43 @@ + + + + Test Panel + + + true + + + Button + true + + + + + Button + true + + + + + Swap Orientation + true + true + + + + + Button + true + + + + + Button + true + true + + + + + + diff --git a/tests/data/test-panel.ui b/tests/data/test-panel.ui new file mode 100644 index 0000000..fdd300d --- /dev/null +++ b/tests/data/test-panel.ui @@ -0,0 +1,363 @@ + + + + Test Panel + 800 + 600 + + + true + true + + + 50 + true + + + + + T_oggle All + true + false + + + + + + Test Grab + true + + + + + + true + + + + dockbin.left-visible + true + + + panel-left-pane-symbolic + true + + + + + + + dockbin.bottom-visible + true + + + panel-bottom-pane-symbolic + true + + + + + + + dockbin.right-visible + true + + + panel-right-pane-symbolic + true + + + + + + + end + + + + + Visible + true + + + end + + + + + true + + + + dockbin.left-pinned + true + + + panel-left-pane-symbolic + true + + + + + + + dockbin.bottom-pinned + true + + + panel-bottom-pane-symbolic + true + + + + + + + dockbin.right-pinned + true + + + panel-right-pane-symbolic + true + + + + + + + end + + + + + Pinned + true + + + end + + + + + + + manager + true + true + true + + + true + true + + + true + true + + + + + + + true + icons + true + true + + + true + true + color-select-symbolic + Color + true + + + 12 + center + true + + + + + + + true + true + Swatches + preferences-color-symbolic + true + + + true + + + + + + + + + true + true + + + Adjustments + true + + + true + true + + + + + + + Styles + true + + + true + 12 + 6 + false + start + + + Cl_ass + class_entry + true + true + + + + + true + true + + + + + + + + + + + true + true + + + true + _Layers + true + + + true + true + + + + + + + true + _Channels + true + + + + + true + _Paths + true + + + + + + + true + true + + + true + true + _Mini Bridge + true + + + true + true + + + + + + + true + true + _Timeline + true + + + true + + + + + + + + + true + + + true + _Files + true + + + true + + + Some Item + 0 + true + + + + + + + + + true + Targets + true + + + + + + + true + + + true + Documentation + true + + + + + + + + + + diff --git a/tests/data/test-slider.ui b/tests/data/test-slider.ui new file mode 100644 index 0000000..b622dbe --- /dev/null +++ b/tests/data/test-slider.ui @@ -0,0 +1,124 @@ + + + DzlSlider Test + 400 + 800 + + + true + + + Top + true + + + + + true + + + + + true + 100 + + + true + Row 1 + + + + + + + 100 + true + + + true + Row 1 + + + + + + + center + center + true + + + true + + + + 1 + 1 + + + + + true + + + pan-start-symbolic + true + + + + + 1 + 0 + + + + + true + + + pan-end-symbolic + true + + + + + 1 + 2 + + + + + true + + + pan-up-symbolic + true + + + + + 0 + 1 + + + + + true + + + pan-down-symbolic + true + + + + + 2 + 1 + + + + + + + + diff --git a/tests/data/test-tab-strip.ui b/tests/data/test-tab-strip.ui new file mode 100644 index 0000000..d744091 --- /dev/null +++ b/tests/data/test-tab-strip.ui @@ -0,0 +1,80 @@ + + + + Test Panel + + + true + vertical + + + true + stack + + + + + true + true + + + true + true + buffer1 + + + tab1 + Tab 1 + + + + + true + true + buffer2 + + + tab2 + Tab 2 + + + + + true + true + buffer3 + + + tab3 + Tab 3 + + + + + true + true + buffer4 + + + tab4 + Tab 4 + + + + + + + + + Page 1 + + + Page 2 + + + Page 3 + + + Page 4 + + diff --git a/tests/data/words.txt b/tests/data/words.txt new file mode 100644 index 0000000..81b14ac --- /dev/null +++ b/tests/data/words.txt @@ -0,0 +1,1000 @@ +1080 +10-point +10th +11-point +12-point +16-point +18-point +1st +2 +20-point +2,4,5-t +2,4-d +2D +2nd +30-30 +3-D +3-d +3D +3M +3rd +48-point +4-D +4GL +4H +4th +5-point +5-T +5th +6-point +6th +7-point +7th +8-point +8th +9-point +9th +-a +A +A. +a +a' +a- +a. +A-1 +A1 +a1 +A4 +A5 +AA +aa +A.A.A. +AAA +aaa +AAAA +AAAAAA +AAAL +AAAS +Aaberg +Aachen +AAE +AAEE +AAF +AAG +aah +aahed +aahing +aahs +AAII +aal +Aalborg +Aalesund +aalii +aaliis +aals +Aalst +Aalto +AAM +aam +AAMSI +Aandahl +A-and-R +Aani +AAO +AAP +AAPSS +Aaqbiye +Aar +Aara +Aarau +AARC +aardvark +aardvarks +aardwolf +aardwolves +Aaren +Aargau +aargh +Aarhus +Aarika +Aaron +aaron +Aaronic +aaronic +Aaronical +Aaronite +Aaronitic +Aaron's-beard +Aaronsburg +Aaronson +AARP +aarrgh +aarrghh +Aaru +AAS +aas +A'asia +aasvogel +aasvogels +AAU +AAUP +AAUW +AAVSO +AAX +A-axes +A-axis +A.B. +AB +Ab +ab +ab- +A.B.A. +ABA +Aba +aba +Ababa +Ababdeh +Ababua +abac +abaca +abacas +abacate +abacaxi +abacay +abaci +abacinate +abacination +abacisci +abaciscus +abacist +aback +abacli +Abaco +abacot +abacterial +abactinal +abactinally +abaction +abactor +abaculi +abaculus +abacus +abacuses +Abad +abada +Abadan +Abaddon +abaddon +abadejo +abadengo +abadia +Abadite +abaff +abaft +Abagael +Abagail +Abagtha +Abailard +abaisance +abaised +abaiser +abaisse +abaissed +abaka +Abakan +abakas +Abakumov +abalation +abalienate +abalienated +abalienating +abalienation +abalone +abalones +Abama +abamp +abampere +abamperes +abamps +Abana +aband +abandon +abandonable +abandoned +abandonedly +abandonee +abandoner +abandoners +abandoning +abandonment +abandonments +abandons +abandum +abanet +abanga +Abanic +abannition +Abantes +abapical +abaptiston +abaptistum +Abarambo +Abarbarea +Abaris +abarthrosis +abarticular +abarticulation +Abas +abas +abase +abased +abasedly +abasedness +abasement +abasements +abaser +abasers +abases +Abasgi +abash +abashed +abashedly +abashedness +abashes +abashing +abashless +abashlessly +abashment +abashments +abasia +abasias +abasic +abasing +abasio +abask +abassi +Abassieh +Abassin +abastard +abastardize +abastral +abatable +abatage +Abate +abate +abated +abatement +abatements +abater +abaters +abates +abatic +abating +abatis +abatised +abatises +abatjour +abatjours +abaton +abator +abators +ABATS +abattage +abattis +abattised +abattises +abattoir +abattoirs +abattu +abattue +Abatua +abature +abaue +abave +abaxial +abaxile +abay +abayah +abaze +abb +Abba +abba +abbacies +abbacomes +abbacy +Abbadide +Abbai +abbandono +abbas +abbasi +Abbasid +abbasid +abbassi +Abbassid +Abbasside +Abbate +abbate +abbatial +abbatical +abbatie +abbaye +Abbe +abbe +abbes +abbess +abbesses +abbest +Abbevilean +Abbeville +Abbevillian +abbevillian +Abbey +abbey +abbeys +abbeystead +abbeystede +Abbi +Abbie +abboccato +abbogada +Abbot +abbot +abbotcies +abbotcy +abbotnullius +abbotric +abbots +Abbotsen +Abbotsford +abbotship +abbotships +Abbotson +Abbotsun +Abbott +abbott +Abbottson +Abbottstown +Abboud +abbozzo +ABBR +abbr +abbrev +abbreviatable +abbreviate +abbreviated +abbreviately +abbreviates +abbreviating +abbreviation +abbreviations +abbreviator +abbreviators +abbreviatory +abbreviature +abbroachment +Abby +abby +Abbye +Abbyville +ABC +abc +abcess +abcissa +abcoulomb +ABCs +abd +abdal +abdali +abdaria +abdat +Abdel +Abd-el-Kadir +Abd-el-Krim +Abdella +Abderhalden +Abderian +Abderite +Abderus +abdest +Abdias +abdicable +abdicant +abdicate +abdicated +abdicates +abdicating +abdication +abdications +abdicative +abdicator +Abdiel +abditive +abditory +abdom +abdomen +abdomens +abdomina +abdominal +Abdominales +abdominales +abdominalia +abdominalian +abdominally +abdominals +abdominoanterior +abdominocardiac +abdominocentesis +abdominocystic +abdominogenital +abdominohysterectomy +abdominohysterotomy +abdominoposterior +abdominoscope +abdominoscopy +abdominothoracic +abdominous +abdomino-uterotomy +abdominovaginal +abdominovesical +Abdon +Abdu +abduce +abduced +abducens +abducent +abducentes +abduces +abducing +abduct +abducted +abducting +abduction +abductions +abductor +abductores +abductors +abducts +Abdul +Abdul-Aziz +Abdul-baha +Abdulla +Abe +a-be +abeam +abear +abearance +Abebi +abecedaire +abecedaria +abecedarian +abecedarians +abecedaries +abecedarium +abecedarius +abecedary +abed +abede +abedge +Abednego +abegge +abeigh +ABEL +Abel +abel +Abelard +abele +abeles +Abelia +Abelian +abelian +Abelicea +Abelite +abelite +Abell +Abelmoschus +abelmosk +abelmosks +abelmusk +Abelonian +Abelson +abeltree +Abencerrages +abend +abends +Abenezra +abenteric +Abeokuta +abepithymia +ABEPP +Abercrombie +Abercromby +Aberdare +aberdavine +Aberdeen +aberdeen +Aberdeenshire +aberdevine +Aberdonian +aberduvine +Aberfan +Aberglaube +Aberia +Abernant +Abernathy +abernethy +Abernon +aberr +aberrance +aberrancies +aberrancy +aberrant +aberrantly +aberrants +aberrate +aberrated +aberrating +aberration +aberrational +aberrations +aberrative +aberrator +aberrometer +aberroscope +Abert +aberuncate +aberuncator +Aberystwyth +abesse +abessive +abet +abetment +abetments +abets +abettal +abettals +abetted +abetter +abetters +abetting +abettor +abettors +Abeu +abevacuation +Abey +abey +abeyance +abeyances +abeyancies +abeyancy +abeyant +abfarad +abfarads +ABFM +Abgatha +ABHC +abhenries +abhenry +abhenrys +abhinaya +abhiseka +abhominable +abhor +abhorred +abhorrence +abhorrences +abhorrency +abhorrent +abhorrently +abhorrer +abhorrers +abhorrible +abhorring +abhors +Abhorson +ABI +Abia +Abiathar +Abib +abib +abichite +abidal +abidance +abidances +abidden +abide +abided +abider +abiders +abides +abidi +abiding +abidingly +abidingness +Abidjan +Abie +abied +abiegh +abience +abient +Abies +abies +abietate +abietene +abietic +abietin +Abietineae +abietineous +abietinic +abietite +Abiezer +Abigael +Abigail +abigail +abigails +abigailship +Abigale +abigeat +abigei +abigeus +Abihu +Abijah +abilao +Abilene +abilene +abiliment +abilitable +abilities +-ability +ability +abilla +abilo +Abilyne +abime +Abimelech +Abineri +Abingdon +Abinger +Abington +Abinoam +Abinoem +abintestate +abiogeneses +abiogenesis +abiogenesist +abiogenetic +abiogenetical +abiogenetically +abiogenist +abiogenous +abiogeny +abiological +abiologically +abiology +abioses +abiosis +abiotic +abiotical +abiotically +abiotrophic +abiotrophy +Abipon +Abiquiu +abir +abirritant +abirritate +abirritated +abirritating +abirritation +abirritative +Abisag +Abisha +Abishag +Abisia +abiston +abit +Abitibi +Abiu +abiuret +Abixah +abject +abjectedness +abjection +abjections +abjective +abjectly +abjectness +abjectnesses +abjoint +abjudge +abjudged +abjudging +abjudicate +abjudicated +abjudicating +abjudication +abjudicator +abjugate +abjunct +abjunction +abjunctive +abjuration +abjurations +abjuratory +abjure +abjured +abjurement +abjurer +abjurers +abjures +abjuring +abkar +abkari +abkary +Abkhas +Abkhasia +Abkhasian +Abkhaz +Abkhazia +Abkhazian +abl +abl. +ablach +ablactate +ablactated +ablactating +ablactation +ablaqueate +ablare +A-blast +ablastemic +ablastin +ablastous +ablate +ablated +ablates +ablating +ablation +ablations +ablatitious +ablatival +ablative +ablatively +ablatives +ablator +ablaut +ablauts +ablaze +-able +able +able-bodied +able-bodiedness +ableeze +ablegate +ablegates +ablegation +able-minded +able-mindedness +ablend +ableness +ablepharia +ablepharon +ablepharous +Ablepharus +ablepsia +ablepsy +ableptical +ableptically +abler +ables +ablesse +ablest +ablet +ablewhackets +ablings +ablins +ablock +abloom +ablow +ABLS +ablude +abluent +abluents +ablush +ablute +abluted +ablution +ablutionary +ablutions +abluvion +-ably +ably +ABM +abmho +abmhos +abmodalities +abmodality +abn +Abnaki +Abnakis +abnegate +abnegated +abnegates +abnegating +abnegation +abnegations +abnegative +abnegator +abnegators +Abner +abner +abnerval +abnet +abneural +abnormal +abnormalcies +abnormalcy +abnormalise +abnormalised +abnormalising +abnormalism +abnormalist +abnormalities +abnormality +abnormalize +abnormalized +abnormalizing +abnormally +abnormalness +abnormals +abnormities +abnormity +abnormous +abnumerable +Abo +abo +aboard +aboardage +Abobra +abococket +abodah +abode +aboded +abodement +abodes +aboding +abody +abogado +abogados +abohm +abohms +aboideau +aboideaus +aboideaux +aboil +aboiteau +aboiteaus +aboiteaux +abolete +abolish +abolishable +abolished +abolisher +abolishers +abolishes +abolishing +abolishment +abolishments +abolition +abolitionary +abolitionise +abolitionised +abolitionising +abolitionism +abolitionist +abolitionists +abolitionize +abolitionized +abolitionizing +abolitions +abolla +abollae +aboma +abomas +abomasa +abomasal +abomasi +abomasum +abomasus +abomasusi +A-bomb +a-bomb +abominability +abominable +abominableness +abominably +abominate +abominated +abominates +abominating +abomination +abominations +abominator +abominators +abomine +abondance +Abongo +abonne +abonnement +aboon +aborad +aboral +aborally +abord +Aboriginal +aboriginal +aboriginality +aboriginally +aboriginals +aboriginary +Aborigine +aborigine +aborigines +Abor-miri +Aborn +a-borning +aborning +aborsement +aborsive +abort +aborted +aborter +aborters +aborticide +abortient +abortifacient +abortin +aborting +abortion +abortional +abortionist +abortionists +abortions +abortive +abortively +abortiveness +abortogenic +aborts +abortus +abortuses +abos +abote +Abott +abouchement +aboudikro +abought +Aboukir +aboulia +aboulias +aboulic +abound +abounded +abounder +abounding +aboundingly +abounds +Abourezk +about +about-face +about-faced +about-facing +abouts +about-ship +about-shipped +about-shipping +about-sledge +about-turn +above +above-board +aboveboard +above-cited +abovedeck diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..a00fdaf --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,373 @@ +if get_option('enable_tests') + +test_env = [ + 'G_TEST_SRCDIR=@0@'.format(meson.current_source_dir()), + 'G_TEST_BUILDDIR=@0@'.format(meson.current_build_dir()), + 'G_DEBUG=gc-friendly', + 'GSETTINGS_BACKEND=memory', + 'PYTHONDONTWRITEBYTECODE=yes', + 'MALLOC_CHECK_=2', +# 'MALLOC_PERTURB_=$((${RANDOM:-256} % 256))', +] + +test_cflags = [ + '-fPIE', + '-DTEST_DATA_DIR="@0@/data"'.format(meson.current_source_dir()), +] + +test_link_args = [ + '-fPIC', +] + +test_application = executable('test-application', 'test-application.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-application', test_application, env: test_env) + +test_menu_manager = executable('test-menu-manager', 'test-menu-manager.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_menu_manager2 = executable('test-menu-manager2', 'test-menu-manager2.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-menu-manager2', test_menu_manager2, env: test_env) + +test_state_machine = executable('test-state-machine', 'test-state-machine.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-state-machine', test_state_machine, env: test_env) + +test_binding_group = executable('test-binding-group', 'test-binding-group.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-binding-group', test_binding_group, env: test_env) + +test_signal_group = executable('test-signal-group', 'test-signal-group.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-signal-group', test_binding_group, env: test_env) + +test_task_cache = executable('test-task-cache', 'test-task-cache.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-task-cache', test_task_cache, env: test_env) + +test_heap = executable('test-heap', 'test-heap.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-heap', test_heap, env: test_env) + +test_radio_box = executable('test-radio-box', 'test-radio-box.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_slider = executable('test-slider', 'test-slider.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_file_chooser_entry = executable('test-file-chooser-entry', 'test-file-chooser-entry.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_elastic_bin = executable('test-elastic-bin', 'test-elastic-bin.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_stack_list = executable('test-stack-list', 'test-stack-list.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_suggestion = executable('test-suggestion', 'test-suggestion.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_suggestion_buffer = executable('test-suggestion-buffer', 'test-suggestion-buffer.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-suggestion-buffer', test_suggestion_buffer, env: test_env) + +test_fuzzy_index = executable('test-fuzzy-index', 'test-fuzzy-index.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-fuzzy-index', test_fuzzy_index, env: test_env) + +test_bin = executable('test-bin', 'test-bin.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_multi_paned = executable('test-multi-paned', 'test-multi-paned.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_tab_strip = executable('test-tab-strip', 'test-tab-strip.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_panel = executable('test-panel', 'test-panel.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_shortcut_chord = executable('test-shortcut-chord', 'test-shortcut-chord.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-shortcut-chord', test_shortcut_chord, env: test_env) + +test_shortcut_overlays = executable('test-shortcut-overlays', 'test-shortcut-overlays.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-shortcut-overlays', test_shortcut_overlays, env: test_env) + +test_shortcut_theme = executable('test-shortcut-theme', 'test-shortcut-theme.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-shortcut-theme', test_shortcut_theme, env: test_env) + +test_shortcuts = executable('test-shortcuts', 'test-shortcuts.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_progress_button = executable('test-progress-button', 'test-progress-button.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_progress_menu_button = executable('test-progress-menu-button', 'test-progress-menu-button.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_progress_icon = executable('test-progress-icon', 'test-progress-icon.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_simple_popover = executable('test-simple-popover', 'test-simple-popover.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_entry_box = executable('test-entry-box', 'test-entry-box.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_bolding_label = executable('test-bolding-label', 'test-bolding-label.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_file_manager = executable('test-file-manager', 'test-file-manager.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_empty_state = executable('test-empty-state', 'test-empty-state.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_fuzzy_mutable_index = executable('test-fuzzy-mutable-index', 'test-fuzzy-mutable-index.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_desktop_index = executable('test-desktop-index', 'test-desktop-index.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_cpu_graph = executable('test-cpu-graph', 'test-cpu-graph.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_trie = executable('test-trie', 'test-trie.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-trie', test_trie, env: test_env) + +test_levenshtein = executable('test-levenshtein', 'test-levenshtein.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-levenshtein', test_levenshtein, env: test_env) + +test_pill_box = executable('test-pill-box', 'test-pill-box.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_preferences = executable('test-preferences', 'test-preferences.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_int_pair = executable('test-int-pair', 'test-int-pair.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-int-pair', test_int_pair, env: test_env) + +test_path_bar = executable('test-path-bar', 'test-path-bar.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_ring = executable('test-ring', 'test-ring.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-ring', test_ring, env: test_env) + +# Counters only run on UNIX-like systems currently +if host_machine.system() != 'windows' +test_counters_window = executable('test-counters-window', 'test-counters-window.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +endif + +test_list_store = executable('test-list-store', 'test-list-store.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-list-store', test_list_store, env: test_env) + +test_joined_menu = executable('test-joined-menu', 'test-joined-menu.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_box = executable('test-box', 'test-box.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_directory_reaper = executable('test-directory-reaper', 'test-directory-reaper.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-directory-reaper', test_directory_reaper, env: test_env) + +test_list_store_adapter = executable('test-list-store-adapter', 'test-list-store-adapter.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-list-store-adapter', test_list_store_adapter, env: test_env) + +test_util = executable('test-util', ['test-util.c', '../src/util/dzl-util.c'], + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-util', test_util, env: test_env) + +test_pattern_spec = executable('test-pattern-spec', 'test-pattern-spec.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-pattern-spec', test_pattern_spec, env: test_env) + +test_recursive_monitor = executable('test-recursive-monitor', 'test-recursive-monitor.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-recursive-monitor', test_recursive_monitor, env: test_env) + +test_file_transfer = executable('test-file-transfer', 'test-file-transfer.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-file-transfer', test_file_transfer, env: test_env) + +test_tree = executable('test-tree', 'test-tree.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) + +test_cancellable = executable('test-cancellable', 'test-cancellable.c', + c_args: test_cflags, + link_args: test_link_args, + dependencies: libdazzle_deps + [libdazzle_dep], +) +test('test-cancellable', test_cancellable, env: test_env) + +endif diff --git a/tests/test-application.c b/tests/test-application.c new file mode 100644 index 0000000..dda2efd --- /dev/null +++ b/tests/test-application.c @@ -0,0 +1,48 @@ +#include +#include + +static gint g_argc; +static gchar **g_argv; + +static gboolean +on_timeout (gpointer data) +{ + DzlApplication *app = data; + g_application_release (G_APPLICATION (app)); + return G_SOURCE_REMOVE; +} + +static void +on_activate (DzlApplication *app) +{ + g_timeout_add_full (G_PRIORITY_LOW, 10, on_timeout, g_object_ref (app), g_object_unref); + g_application_hold (G_APPLICATION (app)); +} + +static void +test_app_basic (void) +{ + g_autoptr(DzlApplication) app = NULL; + int ret; + + app = g_object_new (DZL_TYPE_APPLICATION, + "application-id", "org.gnome.FooTest", + NULL); + g_signal_connect (app, "activate", G_CALLBACK (on_activate), NULL); + ret = g_application_run (G_APPLICATION (app), g_argc, g_argv); + g_assert_cmpint (ret, ==, EXIT_SUCCESS); +} + +gint +main (gint argc, + gchar **argv) +{ + g_test_init (&argc, &argv, NULL); + + g_argc = argc; + g_argv = argv; + + g_test_add_func ("/Dazzle/Application/basic", test_app_basic); + + return g_test_run (); +} diff --git a/tests/test-bin.c b/tests/test-bin.c new file mode 100644 index 0000000..df68c6a --- /dev/null +++ b/tests/test-bin.c @@ -0,0 +1,29 @@ +#include + +gint +main (gint argc, + gchar *argv[]) +{ + GtkWindow *win; + + gtk_init (&argc, &argv); + + win = g_object_new (GTK_TYPE_WINDOW, + "title", "Test Bin", + NULL); + gtk_container_add (GTK_CONTAINER (win), + g_object_new (DZL_TYPE_BIN, + "visible", TRUE, + "child", g_object_new (GTK_TYPE_LABEL, + "label", "Some label for the bin", + "visible", TRUE, + NULL), + NULL) + ); + + g_signal_connect (win, "delete-event", gtk_main_quit, NULL); + gtk_window_present (win); + gtk_main (); + + return 0; +} diff --git a/tests/test-binding-group.c b/tests/test-binding-group.c new file mode 100644 index 0000000..8f79707 --- /dev/null +++ b/tests/test-binding-group.c @@ -0,0 +1,609 @@ + +#define G_LOG_DOMAIN "dzl-binding-group" + +#include + +/* Copied from glib */ +typedef struct _BindingSource +{ + GObject parent_instance; + + gint foo; + gint bar; + gdouble value; + gboolean toggle; +} BindingSource; + +typedef struct _BindingSourceClass +{ + GObjectClass parent_class; +} BindingSourceClass; + +enum +{ + PROP_SOURCE_0, + + PROP_SOURCE_FOO, + PROP_SOURCE_BAR, + PROP_SOURCE_VALUE, + PROP_SOURCE_TOGGLE +}; + +static GType binding_source_get_type (void); +G_DEFINE_TYPE (BindingSource, binding_source, G_TYPE_OBJECT); + +static void +binding_source_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + BindingSource *source = (BindingSource *) gobject; + + switch (prop_id) + { + case PROP_SOURCE_FOO: + source->foo = g_value_get_int (value); + break; + + case PROP_SOURCE_BAR: + source->bar = g_value_get_int (value); + break; + + case PROP_SOURCE_VALUE: + source->value = g_value_get_double (value); + break; + + case PROP_SOURCE_TOGGLE: + source->toggle = g_value_get_boolean (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +binding_source_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + BindingSource *source = (BindingSource *) gobject; + + switch (prop_id) + { + case PROP_SOURCE_FOO: + g_value_set_int (value, source->foo); + break; + + case PROP_SOURCE_BAR: + g_value_set_int (value, source->bar); + break; + + case PROP_SOURCE_VALUE: + g_value_set_double (value, source->value); + break; + + case PROP_SOURCE_TOGGLE: + g_value_set_boolean (value, source->toggle); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +binding_source_class_init (BindingSourceClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->set_property = binding_source_set_property; + gobject_class->get_property = binding_source_get_property; + + g_object_class_install_property (gobject_class, PROP_SOURCE_FOO, + g_param_spec_int ("foo", "Foo", "Foo", + -1, 100, + 0, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, PROP_SOURCE_BAR, + g_param_spec_int ("bar", "Bar", "Bar", + -1, 100, + 0, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, PROP_SOURCE_VALUE, + g_param_spec_double ("value", "Value", "Value", + -100.0, 200.0, + 0.0, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, PROP_SOURCE_TOGGLE, + g_param_spec_boolean ("toggle", "Toggle", "Toggle", + FALSE, + G_PARAM_READWRITE)); +} + +static void +binding_source_init (BindingSource *self) +{ +} + +typedef struct _BindingTarget +{ + GObject parent_instance; + + gint bar; + gdouble value; + gboolean toggle; +} BindingTarget; + +typedef struct _BindingTargetClass +{ + GObjectClass parent_class; +} BindingTargetClass; + +enum +{ + PROP_TARGET_0, + + PROP_TARGET_BAR, + PROP_TARGET_VALUE, + PROP_TARGET_TOGGLE +}; + +static GType binding_target_get_type (void); +G_DEFINE_TYPE (BindingTarget, binding_target, G_TYPE_OBJECT); + +static void +binding_target_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + BindingTarget *target = (BindingTarget *) gobject; + + switch (prop_id) + { + case PROP_TARGET_BAR: + target->bar = g_value_get_int (value); + break; + + case PROP_TARGET_VALUE: + target->value = g_value_get_double (value); + break; + + case PROP_TARGET_TOGGLE: + target->toggle = g_value_get_boolean (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +binding_target_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + BindingTarget *target = (BindingTarget *) gobject; + + switch (prop_id) + { + case PROP_TARGET_BAR: + g_value_set_int (value, target->bar); + break; + + case PROP_TARGET_VALUE: + g_value_set_double (value, target->value); + break; + + case PROP_TARGET_TOGGLE: + g_value_set_boolean (value, target->toggle); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +binding_target_class_init (BindingTargetClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->set_property = binding_target_set_property; + gobject_class->get_property = binding_target_get_property; + + g_object_class_install_property (gobject_class, PROP_TARGET_BAR, + g_param_spec_int ("bar", "Bar", "Bar", + -1, 100, + 0, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, PROP_TARGET_VALUE, + g_param_spec_double ("value", "Value", "Value", + -100.0, 200.0, + 0.0, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, PROP_TARGET_TOGGLE, + g_param_spec_boolean ("toggle", "Toggle", "Toggle", + FALSE, + G_PARAM_READWRITE)); +} + +static void +binding_target_init (BindingTarget *self) +{ +} + +static gboolean +celsius_to_fahrenheit (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data G_GNUC_UNUSED) +{ + gdouble celsius, fahrenheit; + + g_assert (G_VALUE_HOLDS (from_value, G_TYPE_DOUBLE)); + g_assert (G_VALUE_HOLDS (to_value, G_TYPE_DOUBLE)); + + celsius = g_value_get_double (from_value); + fahrenheit = (9 * celsius / 5) + 32.0; + + if (g_test_verbose ()) + g_printerr ("Converting %.2fC to %.2fF\n", celsius, fahrenheit); + + g_value_set_double (to_value, fahrenheit); + + return TRUE; +} + +static gboolean +fahrenheit_to_celsius (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data G_GNUC_UNUSED) +{ + gdouble celsius, fahrenheit; + + g_assert (G_VALUE_HOLDS (from_value, G_TYPE_DOUBLE)); + g_assert (G_VALUE_HOLDS (to_value, G_TYPE_DOUBLE)); + + fahrenheit = g_value_get_double (from_value); + celsius = 5 * (fahrenheit - 32.0) / 9; + + if (g_test_verbose ()) + g_printerr ("Converting %.2fF to %.2fC\n", fahrenheit, celsius); + + g_value_set_double (to_value, celsius); + + return TRUE; +} + +static void +test_binding_group_invalid (void) +{ + DzlBindingGroup *group = dzl_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *target = g_object_new (binding_target_get_type (), NULL); + + /* Invalid Target Property */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*find_property*target_property*!=*NULL*"); + dzl_binding_group_bind (group, "value", + target, "does-not-exist", + G_BINDING_DEFAULT); + g_test_assert_expected_messages (); + + dzl_binding_group_set_source (group, NULL); + + /* Invalid Source Property */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*find_property*source_property*!=*NULL*"); + dzl_binding_group_set_source (group, source); + dzl_binding_group_bind (group, "does-not-exist", + target, "value", + G_BINDING_DEFAULT); + g_test_assert_expected_messages (); + + dzl_binding_group_set_source (group, NULL); + + /* Invalid Source */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*find_property*->source_property*!=*NULL*"); + dzl_binding_group_bind (group, "does-not-exist", + target, "value", + G_BINDING_DEFAULT); + dzl_binding_group_set_source (group, source); + g_test_assert_expected_messages (); + + g_object_unref (target); + g_object_unref (source); + g_object_unref (group); +} + +static void +test_binding_group_default (void) +{ + gsize i, j; + DzlBindingGroup *group = dzl_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *targets[5]; + + for (i = 0; i < G_N_ELEMENTS (targets); ++i) + { + targets[i] = g_object_new (binding_target_get_type (), NULL); + dzl_binding_group_bind (group, "foo", + targets[i], "bar", + G_BINDING_DEFAULT); + } + + g_assert_null (dzl_binding_group_get_source (group)); + dzl_binding_group_set_source (group, source); + g_assert (dzl_binding_group_get_source (group) == (GObject *)source); + + for (i = 0; i < 2; ++i) + { + g_object_set (source, "foo", 42, NULL); + for (j = 0; j < G_N_ELEMENTS (targets); ++j) + g_assert_cmpint (source->foo, ==, targets[j]->bar); + + g_object_set (targets[0], "bar", 47, NULL); + g_assert_cmpint (source->foo, !=, targets[0]->bar); + + /* Check that we transition the source correctly */ + dzl_binding_group_set_source (group, NULL); + g_assert_null (dzl_binding_group_get_source (group)); + dzl_binding_group_set_source (group, source); + g_assert (dzl_binding_group_get_source (group) == (GObject *)source); + } + + g_object_unref (group); + + g_object_set (source, "foo", 0, NULL); + for (i = 0; i < G_N_ELEMENTS (targets); ++i) + g_assert_cmpint (source->foo, !=, targets[i]->bar); + + g_object_unref (source); + for (i = 0; i < G_N_ELEMENTS (targets); ++i) + g_object_unref (targets[i]); +} + +static void +test_binding_group_bidirectional (void) +{ + gsize i, j; + DzlBindingGroup *group = dzl_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *targets[5]; + + for (i = 0; i < G_N_ELEMENTS (targets); ++i) + { + targets[i] = g_object_new (binding_target_get_type (), NULL); + dzl_binding_group_bind (group, "value", + targets[i], "value", + G_BINDING_BIDIRECTIONAL); + } + + g_assert_null (dzl_binding_group_get_source (group)); + dzl_binding_group_set_source (group, source); + g_assert (dzl_binding_group_get_source (group) == (GObject *)source); + + for (i = 0; i < 2; ++i) + { + g_object_set (source, "value", 42.0, NULL); + for (j = 0; j < G_N_ELEMENTS (targets); ++j) + g_assert_cmpfloat (source->value, ==, targets[j]->value); + + g_object_set (targets[0], "value", 47.0, NULL); + g_assert_cmpfloat (source->value, ==, targets[0]->value); + + /* Check that we transition the source correctly */ + dzl_binding_group_set_source (group, NULL); + g_assert_null (dzl_binding_group_get_source (group)); + dzl_binding_group_set_source (group, source); + g_assert (dzl_binding_group_get_source (group) == (GObject *)source); + } + + g_object_unref (group); + + g_object_set (targets[0], "value", 0.0, NULL); + g_assert_cmpfloat (source->value, !=, targets[0]->value); + + g_object_unref (source); + for (i = 0; i < G_N_ELEMENTS (targets); ++i) + g_object_unref (targets[i]); +} + +static void +transform_destroy_notify (gpointer data) +{ + gboolean *transform_destroy_called = data; + + *transform_destroy_called = TRUE; +} + +static void +test_binding_group_transform (void) +{ + gboolean transform_destroy_called = FALSE; + DzlBindingGroup *group = dzl_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *target = g_object_new (binding_target_get_type (), NULL); + + dzl_binding_group_set_source (group, source); + dzl_binding_group_bind_full (group, "value", + target, "value", + G_BINDING_BIDIRECTIONAL, + celsius_to_fahrenheit, + fahrenheit_to_celsius, + &transform_destroy_called, + transform_destroy_notify); + + g_object_set (source, "value", 24.0, NULL); + g_assert_cmpfloat (target->value, ==, ((9 * 24.0 / 5) + 32.0)); + + g_object_set (target, "value", 69.0, NULL); + g_assert_cmpfloat (source->value, ==, (5 * (69.0 - 32.0) / 9)); + + /* The GDestroyNotify should only be called when the + * set is freed, not when the various GBindings are freed + */ + dzl_binding_group_set_source (group, NULL); + g_assert_false (transform_destroy_called); + + g_object_unref (group); + g_assert_true (transform_destroy_called); + + g_object_unref (source); + g_object_unref (target); +} + +static void +test_binding_group_transform_closures (void) +{ + gboolean transform_destroy_called_1 = FALSE; + gboolean transform_destroy_called_2 = FALSE; + DzlBindingGroup *group = dzl_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *target = g_object_new (binding_target_get_type (), NULL); + GClosure *c2f_closure, *f2c_closure; + + c2f_closure = g_cclosure_new (G_CALLBACK (celsius_to_fahrenheit), + &transform_destroy_called_1, + (GClosureNotify) transform_destroy_notify); + f2c_closure = g_cclosure_new (G_CALLBACK (fahrenheit_to_celsius), + &transform_destroy_called_2, + (GClosureNotify) transform_destroy_notify); + + dzl_binding_group_set_source (group, source); + dzl_binding_group_bind_with_closures (group, "value", + target, "value", + G_BINDING_BIDIRECTIONAL, + c2f_closure, + f2c_closure); + + g_object_set (source, "value", 24.0, NULL); + g_assert_cmpfloat (target->value, ==, ((9 * 24.0 / 5) + 32.0)); + + g_object_set (target, "value", 69.0, NULL); + g_assert_cmpfloat (source->value, ==, (5 * (69.0 - 32.0) / 9)); + + /* The GClsoureNotify should only be called when the + * set is freed, not when the various GBindings are freed + */ + dzl_binding_group_set_source (group, NULL); + g_assert_false (transform_destroy_called_1); + g_assert_false (transform_destroy_called_2); + + g_object_unref (group); + g_assert_true (transform_destroy_called_1); + g_assert_true (transform_destroy_called_2); + + g_object_unref (source); + g_object_unref (target); +} + +static void +test_binding_group_same_object (void) +{ + gsize i; + DzlBindingGroup *group = dzl_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), + "foo", 100, + "bar", 50, + NULL); + + dzl_binding_group_set_source (group, source); + dzl_binding_group_bind (group, "foo", + source, "bar", + G_BINDING_BIDIRECTIONAL); + + for (i = 0; i < 2; ++i) + { + g_object_set (source, "foo", 10, NULL); + g_assert_cmpint (source->foo, ==, 10); + g_assert_cmpint (source->bar, ==, 10); + + g_object_set (source, "bar", 30, NULL); + g_assert_cmpint (source->foo, ==, 30); + g_assert_cmpint (source->bar, ==, 30); + + /* Check that it is possible both when initially + * adding the binding and when changing the source + */ + dzl_binding_group_set_source (group, NULL); + dzl_binding_group_set_source (group, source); + } + + g_object_unref (source); + g_object_unref (group); +} + +static void +test_binding_group_weak_ref_source (void) +{ + DzlBindingGroup *group = dzl_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *target = g_object_new (binding_target_get_type (), NULL); + + dzl_binding_group_set_source (group, source); + dzl_binding_group_bind (group, "value", + target, "value", + G_BINDING_BIDIRECTIONAL); + + g_object_add_weak_pointer (G_OBJECT (source), (gpointer)&source); + g_assert (dzl_binding_group_get_source (group) == (GObject *)source); + g_object_unref (source); + g_assert_null (source); + g_assert_null (dzl_binding_group_get_source (group)); + + /* Hopefully this would explode if the binding was still alive */ + g_object_set (target, "value", 0.0, NULL); + + g_object_unref (target); + g_object_unref (group); +} + +static void +test_binding_group_weak_ref_target (void) +{ + DzlBindingGroup *group = dzl_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *target = g_object_new (binding_target_get_type (), NULL); + + dzl_binding_group_set_source (group, source); + dzl_binding_group_bind (group, "value", + target, "value", + G_BINDING_BIDIRECTIONAL); + + g_object_set (source, "value", 47.0, NULL); + g_assert_cmpfloat (target->value, ==, 47.0); + + g_object_add_weak_pointer (G_OBJECT (target), (gpointer)&target); + g_object_unref (target); + g_assert_null (target); + + /* Hopefully this would explode if the binding was still alive */ + g_object_set (source, "value", 0.0, NULL); + + g_object_unref (source); + g_object_unref (group); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/BindingGroup/invalid", test_binding_group_invalid); + g_test_add_func ("/Dazzle/BindingGroup/default", test_binding_group_default); + g_test_add_func ("/Dazzle/BindingGroup/bidirectional", test_binding_group_bidirectional); + g_test_add_func ("/Dazzle/BindingGroup/transform", test_binding_group_transform); + g_test_add_func ("/Dazzle/BindingGroup/transform-closures", test_binding_group_transform_closures); + g_test_add_func ("/Dazzle/BindingGroup/same-object", test_binding_group_same_object); + g_test_add_func ("/Dazzle/BindingGroup/weak-ref-source", test_binding_group_weak_ref_source); + g_test_add_func ("/Dazzle/BindingGroup/weak-ref-target", test_binding_group_weak_ref_target); + return g_test_run (); +} diff --git a/tests/test-bolding-label.c b/tests/test-bolding-label.c new file mode 100644 index 0000000..4052b47 --- /dev/null +++ b/tests/test-bolding-label.c @@ -0,0 +1,66 @@ +#include + +static void +toggle_bold (GtkToggleButton *button, + DzlBoldingLabel *label) +{ + gboolean is_bold = gtk_toggle_button_get_active (button); + + dzl_bolding_label_set_weight (label, is_bold ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL); +} + +int main (int argc, char *argv[]) +{ + GtkWidget *window; + GtkWidget *box; + GtkWidget *label; + GtkWidget *button; + PangoAttrList *attrs; + PangoAttribute *attr; + + gtk_init (&argc, &argv); + + window = g_object_new (GTK_TYPE_WINDOW, + "border-width", 24, + "title", "Test Entry Box", + NULL); + + box = g_object_new (GTK_TYPE_BOX, + "spacing", 12, + "valign", GTK_ALIGN_START, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), box); + + attrs = pango_attr_list_new (); + pango_attr_list_insert (attrs, (attr = pango_attr_style_new (PANGO_STYLE_ITALIC))); + attr->start_index = 0; + attr->end_index = 6; + + label = g_object_new (DZL_TYPE_BOLDING_LABEL, + "attributes", attrs, + "label", "toggle to bold", + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (box), label); + + button = g_object_new (GTK_TYPE_TOGGLE_BUTTON, + "child", g_object_new (GTK_TYPE_IMAGE, + "icon-name", "format-text-bold-symbolic", + "visible", TRUE, + NULL), + "visible", TRUE, + NULL); + gtk_container_add_with_properties (GTK_CONTAINER (box), button, + "pack-type", GTK_PACK_END, + NULL); + + g_signal_connect (button, "clicked", G_CALLBACK (toggle_bold), label); + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + gtk_main (); + + pango_attr_list_unref (attrs); + + return 0; +} diff --git a/tests/test-box.c b/tests/test-box.c new file mode 100644 index 0000000..93a9163 --- /dev/null +++ b/tests/test-box.c @@ -0,0 +1,53 @@ +#include + +gint +main (gint argc, + gchar *argv[]) +{ + GtkWidget *window; + GtkWidget *box; + GtkWidget *label; + + gtk_init (&argc, &argv); + + window = g_object_new (GTK_TYPE_WINDOW, + "title", "Box Test", + NULL); + box = g_object_new (DZL_TYPE_BOX, + "spacing", 12, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), box); + + for (guint i = 0; i < 10; i += 2) + { + g_autofree gchar *str = g_strdup_printf ("%u", i); + + label = g_object_new (GTK_TYPE_LABEL, + "label", str, + "visible", TRUE, + NULL); + gtk_container_add_with_properties (GTK_CONTAINER (box), label, + "position", i, + NULL); + } + + for (guint i = 1; i < 10; i += 2) + { + g_autofree gchar *str = g_strdup_printf ("%u", i); + + label = g_object_new (GTK_TYPE_LABEL, + "label", str, + "visible", TRUE, + NULL); + gtk_container_add_with_properties (GTK_CONTAINER (box), label, + "position", i, + NULL); + } + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + gtk_main (); + + return 0; +} diff --git a/tests/test-cancellable.c b/tests/test-cancellable.c new file mode 100644 index 0000000..9809fa8 --- /dev/null +++ b/tests/test-cancellable.c @@ -0,0 +1,88 @@ +#include + +static void +test_basic (void) +{ + g_autoptr(GCancellable) root = g_cancellable_new (); + g_autoptr(GCancellable) a = g_cancellable_new (); + g_autoptr(GCancellable) b = g_cancellable_new (); + g_autoptr(GCancellable) a1 = g_cancellable_new (); + g_autoptr(GCancellable) a2 = g_cancellable_new (); + + dzl_cancellable_chain (root, a); + dzl_cancellable_chain (root, b); + + dzl_cancellable_chain (a, a1); + dzl_cancellable_chain (a, a2); + + g_cancellable_cancel (a2); + + g_assert_cmpint (TRUE, ==, g_cancellable_is_cancelled (a2)); + g_assert_cmpint (TRUE, ==, g_cancellable_is_cancelled (a)); + g_assert_cmpint (TRUE, ==, g_cancellable_is_cancelled (root)); + + g_assert_cmpint (FALSE, ==, g_cancellable_is_cancelled (a1)); + g_assert_cmpint (FALSE, ==, g_cancellable_is_cancelled (b)); +} + +static void +test_root (void) +{ + g_autoptr(GCancellable) root = g_cancellable_new (); + g_autoptr(GCancellable) a = g_cancellable_new (); + g_autoptr(GCancellable) b = g_cancellable_new (); + g_autoptr(GCancellable) a1 = g_cancellable_new (); + g_autoptr(GCancellable) a2 = g_cancellable_new (); + + dzl_cancellable_chain (root, a); + dzl_cancellable_chain (root, b); + dzl_cancellable_chain (a, a1); + dzl_cancellable_chain (a, a2); + + g_cancellable_cancel (root); + + g_assert_cmpint (TRUE, ==, g_cancellable_is_cancelled (root)); + + g_assert_cmpint (FALSE, ==, g_cancellable_is_cancelled (a)); + g_assert_cmpint (FALSE, ==, g_cancellable_is_cancelled (a1)); + g_assert_cmpint (FALSE, ==, g_cancellable_is_cancelled (a2)); + g_assert_cmpint (FALSE, ==, g_cancellable_is_cancelled (b)); + + g_clear_object (&root); + g_clear_object (&a); + g_clear_object (&b); + g_clear_object (&a1); + g_clear_object (&a2); +} + +static void +test_weak (void) +{ + g_autoptr(GCancellable) root = g_cancellable_new (); + g_autoptr(GCancellable) a = g_cancellable_new (); + g_autoptr(GCancellable) b = g_cancellable_new (); + g_autoptr(GCancellable) a1 = g_cancellable_new (); + g_autoptr(GCancellable) a2 = g_cancellable_new (); + + dzl_cancellable_chain (root, a); + dzl_cancellable_chain (root, b); + dzl_cancellable_chain (a, a1); + dzl_cancellable_chain (a, a2); + + g_clear_object (&root); + g_clear_object (&a); + g_clear_object (&b); + g_clear_object (&a1); + g_clear_object (&a2); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/Cancellable/basic", test_basic); + g_test_add_func ("/Dazzle/Cancellable/root", test_root); + g_test_add_func ("/Dazzle/Cancellable/weak", test_weak); + return g_test_run (); +} diff --git a/tests/test-counters-window.c b/tests/test-counters-window.c new file mode 100644 index 0000000..88f4595 --- /dev/null +++ b/tests/test-counters-window.c @@ -0,0 +1,67 @@ +#include +#include +#include + +static gboolean +int_parse_with_range (gint *value, + gint lower, + gint upper, + const gchar *str) +{ + gint64 v64; + + g_assert (value); + g_assert (lower <= upper); + + v64 = g_ascii_strtoll (str, NULL, 10); + + if (((v64 == G_MININT64) || (v64 == G_MAXINT64)) && (errno == ERANGE)) + return FALSE; + + if ((v64 < lower) || (v64 > upper)) + return FALSE; + + *value = (gint)v64; + + return TRUE; +} + +gint +main (gint argc, + gchar *argv[]) +{ + GtkWidget *window; + DzlCounterArena *arena; + gint pid; + + gtk_init (&argc, &argv); + + if (argc < 2) + { + g_printerr ("usage: %s [PID]\n", argv[0]); + return EXIT_FAILURE; + } + + if (!int_parse_with_range (&pid, 1, G_MAXUSHORT, argv[1])) + { + g_printerr ("usage: %s [PID]\n", argv[0]); + return EXIT_FAILURE; + } + + arena = dzl_counter_arena_new_for_pid (pid); + + if (arena == NULL) + { + g_printerr ("Failed to access counters for process %u.\n", pid); + return EXIT_FAILURE; + } + + window = dzl_counters_window_new (); + dzl_counters_window_set_arena (DZL_COUNTERS_WINDOW (window), arena); + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + + gtk_main (); + + return EXIT_SUCCESS; +} diff --git a/tests/test-cpu-graph.c b/tests/test-cpu-graph.c new file mode 100644 index 0000000..6e97bab --- /dev/null +++ b/tests/test-cpu-graph.c @@ -0,0 +1,73 @@ +#include +#include + +int +main (int argc, + char *argv[]) +{ + guint samples = 2; + guint seconds = 30; + const GOptionEntry entries[] = { + { "samples", 'm', 0, G_OPTION_ARG_INT, &samples, "Number of samples per second", "2" }, + { "seconds", 's', 0, G_OPTION_ARG_INT, &seconds, "Number of seconds to display", "60" }, + { NULL } + }; + gint64 timespan; + guint max_samples; + GOptionContext *context; + GtkWindow *window; + GtkBox *box; + DzlGraphView *graph; + GtkCssProvider *provider; + GError *error = NULL; + + context = g_option_context_new ("- a simple cpu graph"); + g_option_context_add_group (context, gtk_get_option_group (TRUE)); + g_option_context_add_main_entries (context, entries, NULL); + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("%s\n", error->message); + return EXIT_FAILURE; + } + + g_print ("%d samples per second over %d seconds.\n", + samples, seconds); + + timespan = (gint64)seconds * G_USEC_PER_SEC; + max_samples = seconds * samples; + + provider = dzl_css_provider_new ("resource:///org/gnome/dazzle/themes"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref (provider); + + window = g_object_new (GTK_TYPE_WINDOW, + "default-width", 600, + "default-height", 325, + "title", "CPU Graph", + NULL); + + box = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_VERTICAL, + "visible", TRUE, + "spacing", 3, + NULL); + gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (box)); + + for (int i = 0; i < 3; i++) + { + graph = g_object_new (DZL_TYPE_CPU_GRAPH, + "visible", TRUE, + "vexpand", TRUE, + "timespan", timespan, + "max-samples", max_samples, + NULL); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (graph)); + } + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (window); + gtk_main (); + + return 0; +} diff --git a/tests/test-desktop-index.c b/tests/test-desktop-index.c new file mode 100644 index 0000000..b1f7909 --- /dev/null +++ b/tests/test-desktop-index.c @@ -0,0 +1,375 @@ +#include +#include + +static GTimer *timer; +static gchar *last_query; + +static void +query_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GListModel) model = NULL; + DzlFuzzyIndex *index = (DzlFuzzyIndex *)object; + g_autoptr(GError) error = NULL; + g_autoptr(GListStore) suggestions = NULL; + g_autoptr(DzlSuggestionEntry) entry = user_data; + g_autoptr(GHashTable) hash = NULL; + guint n_items; + + model = dzl_fuzzy_index_query_finish (index, result, &error); + + if (error) + { + g_printerr ("%s\n", error->message); + return; + } + + suggestions = g_list_store_new (DZL_TYPE_SUGGESTION); + + n_items = g_list_model_get_n_items (model); + + hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + for (guint i = 0; i < n_items; i++) + { + g_autoptr(DzlFuzzyIndexMatch) match = g_list_model_get_item (model, i); + GVariant *doc = dzl_fuzzy_index_match_get_document (match); + g_autoptr(DzlSuggestion) suggestion = NULL; + g_autoptr(GVariantDict) dict = g_variant_dict_new (doc); + const gchar *id = NULL; + const gchar *title = NULL; + const gchar *icon_name = NULL; + g_autofree gchar *highlight = NULL; + g_autofree gchar *escaped = NULL; + g_autofree gchar *subtitle = NULL; + g_autofree gchar *escape_keyword = NULL; + + if (!g_variant_dict_lookup (dict, "id", "&s", &id)) + id = NULL; + if (!g_variant_dict_lookup (dict, "title", "&s", &title)) + title = NULL; + if (!g_variant_dict_lookup (dict, "icon-name", "&s", &icon_name)) + icon_name = NULL; + + if (g_hash_table_contains (hash, id)) + continue; + else + g_hash_table_insert (hash, g_strdup (id), NULL); + + escaped = g_markup_escape_text (title, -1); + highlight = dzl_fuzzy_highlight (escaped, last_query, FALSE); + escape_keyword = g_markup_escape_text (dzl_fuzzy_index_match_get_key (match), -1); + subtitle = g_strdup_printf ("%lf (%s) (priority %u)", + dzl_fuzzy_index_match_get_score (match), + escape_keyword, + dzl_fuzzy_index_match_get_priority (match)); + + suggestion = g_object_new (DZL_TYPE_SUGGESTION, + "id", id, + "icon-name", icon_name, + "title", highlight, + "subtitle", subtitle, + NULL); + + g_list_store_append (suggestions, suggestion); + } + + dzl_suggestion_entry_set_model (entry, G_LIST_MODEL (suggestions)); +} + +static void +entry_changed (DzlSuggestionEntry *entry, + DzlFuzzyIndex *index) +{ + const gchar *typed_text; + GString *str = g_string_new (NULL); + + g_assert (DZL_IS_SUGGESTION_ENTRY (entry)); + g_assert (DZL_IS_FUZZY_INDEX (index)); + + typed_text = dzl_suggestion_entry_get_typed_text (entry); + + g_timer_reset (timer); + + for (; *typed_text; typed_text = g_utf8_next_char (typed_text)) + { + gunichar ch = g_utf8_get_char (typed_text); + + if (!g_unichar_isspace (ch)) + g_string_append_unichar (str, ch); + } + + dzl_fuzzy_index_query_async (index, str->str, 25, NULL, query_cb, g_object_ref (entry)); + + g_free (last_query); + last_query = g_string_free (str, FALSE); +} + +static void +create_ui (void) +{ + GtkWindow *window; + GtkHeaderBar *header; + GtkWidget *entry; + g_autoptr(DzlFuzzyIndex) index = dzl_fuzzy_index_new (); + g_autoptr(GFile) file = g_file_new_for_path ("desktop.index"); + g_autoptr(GError) error = NULL; + g_autoptr(GtkCssProvider) provider = NULL; + gboolean r; + + provider = dzl_css_provider_new ("resource:///org/gnome/dazzle/themes"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION-1); + + timer = g_timer_new (); + + r = dzl_fuzzy_index_load_file (index, file, NULL, &error); + g_assert_no_error (error); + g_assert (r); + + window = g_object_new (GTK_TYPE_WINDOW, + "title", "Title Window", + "default-width", 800, + "default-height", 600, + NULL); + + header = g_object_new (GTK_TYPE_HEADER_BAR, + "show-close-button", TRUE, + "visible", TRUE, + NULL); + gtk_window_set_titlebar (window, GTK_WIDGET (header)); + + entry = g_object_new (DZL_TYPE_SUGGESTION_ENTRY, + "visible", TRUE, + "max-width-chars", 55, + NULL); + gtk_header_bar_set_custom_title (header, entry); + + g_signal_connect_data (entry, + "changed", + G_CALLBACK (entry_changed), + g_steal_pointer (&index), + (GClosureNotify)g_object_unref, + 0); + + gtk_widget_grab_focus (entry); + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (window); + +} + +static gchar ** +tokenize (const gchar *input) +{ + return g_strsplit_set (input, ",;: -_./\t\n", 0); +} + +static void +test_desktop_index_key (DzlFuzzyIndexBuilder *builder, + GKeyFile *key_file, + const gchar *group, + const gchar *key, + GVariant *document, + gboolean can_tokenize, + gint priority) +{ + g_autofree gchar *str = NULL; + g_auto(GStrv) words = NULL; + + g_assert (DZL_IS_FUZZY_INDEX_BUILDER (builder)); + g_assert (group); + g_assert (key); + g_assert (document); + + if (NULL == (str = g_key_file_get_string (key_file, group, key, NULL))) + return; + + if (!can_tokenize) + { + dzl_fuzzy_index_builder_insert (builder, str, document, priority); + return; + } + + words = tokenize (str); + + for (guint i = 0; words[i] != NULL; i++) + { + if (*words[i] && !g_ascii_isspace (*words[i])) + dzl_fuzzy_index_builder_insert (builder, words[i], document, priority); + } +} + +static gboolean +test_desktop_index_file (DzlFuzzyIndexBuilder *builder, + const gchar *path, + const gchar *name, + GError **error) +{ + g_autoptr(GKeyFile) key_file = NULL; + g_autoptr(GVariant) document = NULL; + g_autofree gchar *icon = NULL; + g_autofree gchar *entry_name = NULL; + GVariantDict dict; + + g_assert (DZL_IS_FUZZY_INDEX_BUILDER (builder)); + g_assert (path != NULL); + + key_file = g_key_file_new (); + + if (!g_key_file_load_from_file (key_file, path, 0, error)) + return FALSE; + + if (!g_key_file_has_group (key_file, "Desktop Entry")) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s is not a desktop file", + path); + return FALSE; + } + + icon = g_key_file_get_string (key_file, "Desktop Entry", "Icon", NULL); + entry_name = g_key_file_get_string (key_file, "Desktop Entry", "Name", NULL); + + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "id", "s", name ?: ""); + g_variant_dict_insert (&dict, "icon-name", "s", icon ?: ""); + g_variant_dict_insert (&dict, "title", "s", entry_name ?: ""); + document = g_variant_take_ref (g_variant_dict_end (&dict)); + + test_desktop_index_key (builder, key_file, "Desktop Entry", "Name", document, FALSE, 0); + test_desktop_index_key (builder, key_file, "Desktop Entry", "Name", document, TRUE, 1); + test_desktop_index_key (builder, key_file, "Desktop Entry", "GenericName", document, TRUE, 2); + test_desktop_index_key (builder, key_file, "Desktop Entry", "Keywords", document, TRUE, 3); + test_desktop_index_key (builder, key_file, "Desktop Entry", "Comment", document, TRUE, 4); + test_desktop_index_key (builder, key_file, "Desktop Entry", "Categories", document, TRUE, 5); + + return TRUE; +} + +static void +fsck_index (void) +{ + g_autofree gchar *contents = NULL; + g_autoptr(GVariant) variant = NULL; + g_autoptr(GVariantIter) iter = NULL; + const gchar *key; + GVariant *value; + GVariantDict dict; + gsize len; + GError *error = NULL; + gboolean r; + + r = g_file_get_contents ("desktop.index", &contents, &len, &error); + g_assert_no_error (error); + g_assert (r); + + variant = g_variant_new_from_data (G_VARIANT_TYPE_VARDICT, contents, len, FALSE, NULL, NULL); + g_assert (variant != NULL); + g_variant_take_ref (variant); + + g_variant_dict_init (&dict, variant); + + r = g_variant_dict_lookup (&dict, "tables", "a{sv}" , &iter); + g_assert (r); + + while (g_variant_iter_loop (iter, "{sv}", &key, &value)) + { + g_autoptr(GVariantIter) aiter = NULL; + guint last_key_id = 0; + gint last_offset = -1; + guint key_id; + guint offset; + + aiter = g_variant_iter_new (value); + g_assert (aiter != NULL); + + while (g_variant_iter_loop (aiter, "(uu)", &offset, &key_id)) + { + g_assert_cmpint (key_id, >=, last_key_id); + if (key_id == last_key_id) + g_assert_cmpint (offset, >, last_offset); + last_key_id = key_id; + last_offset = offset; + } + } + + g_variant_dict_clear (&dict); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_autoptr(GOptionContext) context = NULL; + g_autoptr(DzlFuzzyIndexBuilder) builder = NULL; + g_autoptr(GFile) outfile = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GPtrArray) ar = NULL; + + context = g_option_context_new ("[DIRECTORIES...] - Index desktop info directories"); + g_option_context_add_group (context, gtk_get_option_group (TRUE)); + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("%s\n", error->message); + return EXIT_FAILURE; + } + + builder = dzl_fuzzy_index_builder_new (); + + dzl_fuzzy_index_builder_set_case_sensitive (builder, FALSE); + + ar = g_ptr_array_new (); + for (guint i = 1; i < argc; i++) + g_ptr_array_add (ar, argv[i]); + if (ar->len == 0) + g_ptr_array_add (ar, "/usr/share/applications"); + + for (guint i = 0; i < ar->len; i++) + { + const gchar *name; + const gchar *directory = g_ptr_array_index (ar, i); + g_autoptr(GDir) dir = g_dir_open (directory, 0, &error); + + if (dir == NULL) + { + g_printerr ("%s\n", error->message); + g_clear_error (&error); + continue; + } + + while (NULL != (name = g_dir_read_name (dir))) + { + g_autofree gchar *path = NULL; + + if (!g_str_has_suffix (name, ".desktop")) + continue; + + path = g_build_filename (directory, name, NULL); + + if (!test_desktop_index_file (builder, path, name, &error)) + { + g_printerr ("%s\n", error->message); + g_clear_error (&error); + continue; + } + } + } + + outfile = g_file_new_for_path ("desktop.index"); + + if (!dzl_fuzzy_index_builder_write (builder, outfile, G_PRIORITY_DEFAULT, NULL, &error)) + g_printerr ("%s\n", error->message); + + g_print ("desktop.index.written\n"); + + fsck_index (); + create_ui (); + gtk_main (); + + return EXIT_SUCCESS; +} diff --git a/tests/test-directory-reaper.c b/tests/test-directory-reaper.c new file mode 100644 index 0000000..4cf2ae7 --- /dev/null +++ b/tests/test-directory-reaper.c @@ -0,0 +1,98 @@ +/* test-directory-reaper.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#include +#include +#include + +typedef struct +{ + /* the test data path */ + const gchar *path; + + /* null for a directory, otherwise file contents */ + const gchar *data; +} FileInfo; + +static void +test_reaper_basic (void) +{ + g_autoptr(DzlDirectoryReaper) reaper = dzl_directory_reaper_new (); + g_autoptr(GFile) file = g_file_new_for_path ("reaper"); + g_autoptr(GError) error = NULL; + gboolean r; + static const FileInfo files[] = { + /* directories first */ + { "a/b/c" }, + { "a/c/b" }, + { "a/d/e" }, + + /* then files */ + { "a/b/c/f", "" }, + { "a/c/b/g", "" }, + { "a/d/e/h", "" }, + }; + + /* + * Start out by creating some directories and files so that we can + * test that they've been reaped correctly. + */ + for (guint i = 0; i < G_N_ELEMENTS (files); i++) + { + const FileInfo *info = &files[i]; + g_autofree gchar *path = g_build_filename ("reaper", info->path, NULL); + + if (info->data == NULL) + { + r = g_mkdir_with_parents (path, 0750); + g_assert_cmpint (r, ==, 0); + continue; + } + + r = g_file_set_contents (path, info->data, -1, &error); + g_assert_no_error (error); + g_assert_cmpint (r, ==, TRUE); + } + + /* Add a symlink to ../ so that we keep ourselves honest ;) */ + { + g_autofree gchar *cwd = g_get_current_dir (); + g_autofree gchar *name = g_build_filename ("reaper", "parent-link", NULL); + + if (symlink (cwd, name) != 0) + g_error ("Failed to create symlink"); + } + + dzl_directory_reaper_add_directory (reaper, file, 0); + + r = dzl_directory_reaper_execute (reaper, NULL, &error); + g_assert_no_error (error); + g_assert_cmpint (r, ==, TRUE); + + if (g_rmdir ("reaper") != 0) + g_error ("Failed to remove 'reaper': %s", g_strerror (errno)); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/DirectoryReaper/basic", test_reaper_basic); + return g_test_run (); +} diff --git a/tests/test-elastic-bin.c b/tests/test-elastic-bin.c new file mode 100644 index 0000000..7d3284a --- /dev/null +++ b/tests/test-elastic-bin.c @@ -0,0 +1,72 @@ +#include + +static GtkWidget *list_box; + +static void +add_row (void) +{ + static guint counter; + g_autofree gchar *text = g_strdup_printf ("%u", ++counter); + GtkWidget *row; + + row = g_object_new (GTK_TYPE_LABEL, + "label", text, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (list_box), row); +} + +gint +main (gint argc, + gchar *argv[]) +{ + GtkWidget *window; + GtkWidget *box; + GtkWidget *button; + GtkWidget *scroller; + GtkWidget *bin; + + gtk_init (&argc, &argv); + + window = g_object_new (GTK_TYPE_WINDOW, + "title", "test-elastic", + "default-width", 300, + "resizable", FALSE, + NULL); + + box = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_VERTICAL, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), box); + + button = g_object_new (GTK_TYPE_BUTTON, + "label", "Add row", + "visible", TRUE, + NULL); + g_signal_connect (button, "clicked", add_row, NULL); + gtk_container_add (GTK_CONTAINER (box), button); + + bin = g_object_new (DZL_TYPE_ELASTIC_BIN, + "vexpand", TRUE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (box), bin); + + scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW, + "propagate-natural-height", TRUE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (bin), scroller); + + list_box = g_object_new (GTK_TYPE_LIST_BOX, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (scroller), list_box); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + gtk_main (); + + return 0; +} diff --git a/tests/test-empty-state.c b/tests/test-empty-state.c new file mode 100644 index 0000000..bffd72d --- /dev/null +++ b/tests/test-empty-state.c @@ -0,0 +1,33 @@ +#include + +gint +main (gint argc, + gchar *argv[]) +{ + GtkWidget *window; + GtkWidget *state; + + gtk_init (&argc, &argv); + + window = g_object_new (GTK_TYPE_WINDOW, + "default-width", 800, + "default-height", 600, + "title", "Test Empty State", + NULL); + + state = g_object_new (DZL_TYPE_EMPTY_STATE, + "icon-name", "face-sick-symbolic", + "pixel-size", 192, + "visible", TRUE, + "title", "No Recordings", + "subtitle", "Click record to begin a new recording", + NULL); + gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (state)); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + + gtk_main (); + + return 0; +} diff --git a/tests/test-entry-box.c b/tests/test-entry-box.c new file mode 100644 index 0000000..331ae97 --- /dev/null +++ b/tests/test-entry-box.c @@ -0,0 +1,44 @@ +#include + +int main (int argc, char *argv[]) +{ + GtkWidget *window; + GtkWidget *entry_box; + GtkWidget *label; + GtkWidget *icon; + + gtk_init (&argc, &argv); + + window = g_object_new (GTK_TYPE_WINDOW, + "border-width", 24, + "title", "Test Entry Box", + NULL); + + entry_box = g_object_new (DZL_TYPE_ENTRY_BOX, + "max-width-chars", 55, + "spacing", 12, + "valign", GTK_ALIGN_START, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), entry_box); + + label = g_object_new (GTK_TYPE_LABEL, + "label", "Frobnicate", + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (entry_box), label); + + icon = g_object_new (DZL_TYPE_PROGRESS_ICON, + "progress", .33, + "visible", TRUE, + NULL); + gtk_container_add_with_properties (GTK_CONTAINER (entry_box), icon, + "pack-type", GTK_PACK_END, + NULL); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + gtk_main (); + + return 0; +} diff --git a/tests/test-file-chooser-entry.c b/tests/test-file-chooser-entry.c new file mode 100644 index 0000000..ff52d31 --- /dev/null +++ b/tests/test-file-chooser-entry.c @@ -0,0 +1,101 @@ +#include + +gint +main (gint argc, + gchar *argv[]) +{ + static const gchar *bool_properties[] = { + "local-only", + "create-folders", + "do-overwrite-confirmation", + "show-hidden", + NULL + }; + static const gchar *int_properties[] = { + "max-width-chars", + NULL, + }; + GtkWindow *window; + GtkBox *box; + GtkBox *vbox; + DzlFileChooserEntry *entry; + GFile *file; + guint i; + + gtk_init (&argc, &argv); + + window = g_object_new (GTK_TYPE_WINDOW, + "title", "Test DzlFileChooserEntry", + "border-width", 24, + NULL); + + box = g_object_new (GTK_TYPE_BOX, + "valign", GTK_ALIGN_CENTER, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "spacing", 36, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (box)); + + vbox = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_VERTICAL, + "halign", GTK_ALIGN_START, + "visible", TRUE, + "spacing", 6, + NULL); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (vbox)); + + entry = g_object_new (DZL_TYPE_FILE_CHOOSER_ENTRY, + "title", "Select a Folder", + "action", GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, + "valign", GTK_ALIGN_CENTER, + "visible", TRUE, + NULL); + + for (i = 0; bool_properties [i]; i++) + { + GtkCheckButton *button; + + button = g_object_new (GTK_TYPE_CHECK_BUTTON, + "label", bool_properties[i], + "visible", TRUE, + "halign", GTK_ALIGN_START, + NULL); + g_object_bind_property (button, "active", entry, bool_properties[i], G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + gtk_container_add (GTK_CONTAINER (vbox), GTK_WIDGET (button)); + } + + for (i = 0; int_properties [i]; i++) + { + GtkAdjustment *adj; + GtkSpinButton *button; + GParamSpec *pspec; + + pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (entry), int_properties [i]); + adj = g_object_new (GTK_TYPE_ADJUSTMENT, + "lower", (gdouble)((GParamSpecInt*)pspec)->minimum, + "upper", (gdouble)((GParamSpecInt*)pspec)->maximum, + "value", (gdouble)((GParamSpecInt*)pspec)->default_value, + "step-increment", 1.0, + NULL); + button = g_object_new (GTK_TYPE_SPIN_BUTTON, + "adjustment", adj, + "visible", TRUE, + "halign", GTK_ALIGN_START, + NULL); + g_object_bind_property (button, "value", entry, int_properties[i], G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); + gtk_container_add (GTK_CONTAINER (vbox), GTK_WIDGET (button)); + } + + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (entry)); + + file = g_file_new_for_path (g_get_home_dir ()); + dzl_file_chooser_entry_set_file (entry, file); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (window); + + gtk_main (); + + return 0; +} diff --git a/tests/test-file-manager.c b/tests/test-file-manager.c new file mode 100644 index 0000000..e6b9d9b --- /dev/null +++ b/tests/test-file-manager.c @@ -0,0 +1,24 @@ +#include +#include + +int main (int argc, char *argv[]) +{ + g_autoptr(GFile) file = NULL; + g_autoptr(GError) error = NULL; + + if (argc == 1) + { + g_printerr ("usage: %s FILENAME\n", argv[0]); + return EXIT_FAILURE; + } + + file = g_file_new_for_commandline_arg (argv [1]); + + if (!dzl_file_manager_show (file, &error)) + { + g_printerr ("%s\n", error->message); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/tests/test-file-transfer.c b/tests/test-file-transfer.c new file mode 100644 index 0000000..0f512d1 --- /dev/null +++ b/tests/test-file-transfer.c @@ -0,0 +1,82 @@ +#include +#include + +static void +write_file (const gchar *path) +{ + g_autoptr(GFile) file = g_file_new_for_path (path); + g_autoptr(GOutputStream) stream = NULL; + g_autoptr(GError) error = NULL; + gsize len = 0; + + stream = G_OUTPUT_STREAM (g_file_create (file, G_FILE_CREATE_NONE, NULL, &error)); + g_assert_no_error (error); + g_assert (G_IS_OUTPUT_STREAM (stream)); + g_output_stream_write_all (stream, "some-data", strlen ("some-data"), &len, NULL, &error); + g_assert_no_error (error); + g_assert_cmpint (len, ==, strlen ("some-data")); +} + +static void +test_basic (void) +{ + g_autoptr(DzlFileTransfer) xfer = dzl_file_transfer_new (); + g_autoptr(GFile) root = g_file_new_for_path ("test-file-transfer-data"); + g_autoptr(GFile) copy = g_file_new_for_path ("test-file-transfer-copy"); + g_autoptr(GError) error = NULL; + g_autoptr(DzlDirectoryReaper) reaper = dzl_directory_reaper_new (); + gboolean r; + + dzl_directory_reaper_add_directory (reaper, root, 0); + dzl_directory_reaper_add_directory (reaper, copy, 0); + dzl_directory_reaper_add_file (reaper, root, 0); + dzl_directory_reaper_add_file (reaper, copy, 0); + dzl_directory_reaper_execute (reaper, NULL, NULL); + g_assert (!g_file_query_exists (root, NULL)); + g_assert (!g_file_query_exists (copy, NULL)); + + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/a", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/a/1", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/a/1/a", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/a/1/b", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/a/1/c", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/a/2", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/a/2/a", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/a/2/b", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/a/2/c", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/b", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/b/1", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/b/1/a", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/b/1/b", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/b/1/c", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/c", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/c/1", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/c/1/a", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/c/1/b", 0750)); + g_assert_cmpint (0, ==, g_mkdir ("test-file-transfer-data/c/1/c", 0750)); + + write_file ("test-file-transfer-data/z"); + write_file ("test-file-transfer-data/a/z"); + write_file ("test-file-transfer-data/b/1/c/z"); + write_file ("test-file-transfer-data/c/1/c/z"); + + dzl_file_transfer_set_flags (xfer, DZL_FILE_TRANSFER_FLAGS_MOVE); + dzl_file_transfer_add (xfer, root, copy); + r = dzl_file_transfer_execute (xfer, G_PRIORITY_DEFAULT, NULL, &error); + g_assert_no_error (error); + g_assert_cmpint (r, ==, TRUE); + + dzl_directory_reaper_execute (reaper, NULL, NULL); + g_assert (!g_file_query_exists (root, NULL)); + g_assert (!g_file_query_exists (copy, NULL)); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/FileTransfer/basic", test_basic); + return g_test_run (); +} diff --git a/tests/test-fuzzy-index.c b/tests/test-fuzzy-index.c new file mode 100644 index 0000000..8c7d61b --- /dev/null +++ b/tests/test-fuzzy-index.c @@ -0,0 +1,264 @@ +/* test-writer.c + * + * Copyright (C) 2016 Christian Hergert + * + * 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 . + */ + +#include + +static GMainLoop *main_loop; + +static void +test_index_builder_basic_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + DzlFuzzyIndexBuilder *builder = (DzlFuzzyIndexBuilder *)object; + GError **error = user_data; + gboolean r; + + r = dzl_fuzzy_index_builder_write_finish (builder, result, error); + g_assert (r == TRUE); + g_main_loop_quit (main_loop); +} + +static void +test_index_builder_basic (void) +{ + DzlFuzzyIndexBuilder *builder; + gchar *contents = NULL; + g_autoptr(GVariant) variant = NULL; + GVariantDict dict; + GVariant *v; + GError *error = NULL; + GFile *file; + gboolean r; + gsize len; + + main_loop = g_main_loop_new (NULL, FALSE); + + file = g_file_new_for_path ("index.gvariant"); + + builder = dzl_fuzzy_index_builder_new (); + + dzl_fuzzy_index_builder_insert (builder, "foo", g_variant_new_int32 (1), 7); + dzl_fuzzy_index_builder_insert (builder, "FOO", g_variant_new_int32 (2), 7); + dzl_fuzzy_index_builder_insert (builder, "Foo", g_variant_new_int32 (3), 7); + dzl_fuzzy_index_builder_insert (builder, "bar", g_variant_new_int32 (4), 7); + dzl_fuzzy_index_builder_insert (builder, "baz", g_variant_new_int32 (5), 7); + + dzl_fuzzy_index_builder_write_async (builder, + file, + G_PRIORITY_LOW, + NULL, + test_index_builder_basic_cb, + &error); + + g_main_loop_run (main_loop); + g_assert_no_error (error); + + r = g_file_load_contents (file, NULL, &contents, &len, NULL, &error); + g_assert_no_error (error); + g_assert (r); + + variant = g_variant_new_from_data (G_VARIANT_TYPE_VARDICT, + contents, + len, + FALSE, + g_free, + contents); + g_assert (variant != NULL); + g_variant_ref_sink (variant); + +#if 0 + g_print ("%s\n", g_variant_print (variant, TRUE)); +#endif + + g_variant_dict_init (&dict, variant); + + v = g_variant_dict_lookup_value (&dict, "version", G_VARIANT_TYPE_INT32); + g_assert (v != NULL); + g_variant_unref (v); + + v = g_variant_dict_lookup_value (&dict, "tables", G_VARIANT_TYPE_VARDICT); + g_assert (v != NULL); + g_variant_unref (v); + + v = g_variant_dict_lookup_value (&dict, "keys", G_VARIANT_TYPE_ARRAY); + g_assert (v != NULL); + g_variant_unref (v); + + v = g_variant_dict_lookup_value (&dict, "lookaside", G_VARIANT_TYPE_ARRAY); + g_assert (v != NULL); + g_variant_unref (v); + + v = g_variant_dict_lookup_value (&dict, "documents", G_VARIANT_TYPE_ARRAY); + g_assert (v != NULL); + g_variant_unref (v); + + g_variant_dict_clear (&dict); + + g_object_unref (builder); + + r = g_file_delete (file, NULL, &error); + g_assert_no_error (error); + g_assert (r); + + g_object_unref (file); + + while (g_main_context_pending (NULL)) + g_main_context_iteration (NULL, TRUE); + + g_main_loop_unref (main_loop); +} + +static void +test_index_basic_query_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + DzlFuzzyIndex *index = (DzlFuzzyIndex *)object; + g_autoptr(GFile) file = user_data; + g_autoptr(GListModel) matches = NULL; + GError *error = NULL; + guint n_items; + guint i; + + matches = dzl_fuzzy_index_query_finish (index, result, &error); + g_assert_no_error (error); + g_assert (matches != NULL); + + n_items = g_list_model_get_n_items (matches); + g_assert_cmpint (n_items, ==, 5); + g_assert (DZL_TYPE_FUZZY_INDEX_MATCH == g_list_model_get_item_type (matches)); + + for (i = 0; i < n_items; i++) + { + g_autoptr(DzlFuzzyIndexMatch) match = g_list_model_get_item (matches, i); + const gchar *key; + GVariant *doc; + gfloat score; + + g_assert (DZL_IS_FUZZY_INDEX_MATCH (match)); + + key = dzl_fuzzy_index_match_get_key (match); + doc = dzl_fuzzy_index_match_get_document (match); + score = dzl_fuzzy_index_match_get_score (match); + + if (FALSE) + { + g_autofree gchar *format = g_variant_print (doc, TRUE); + g_print ("%f %s %s\n", score, key, format); + } + } + + g_main_loop_quit (main_loop); +} + +static void +test_index_basic_load_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + DzlFuzzyIndex *index = (DzlFuzzyIndex *)object; + g_autoptr(GFile) file = user_data; + GError *error = NULL; + gboolean r; + + r = dzl_fuzzy_index_load_file_finish (index, result, &error); + g_assert_no_error (error); + g_assert_cmpint (r, ==, TRUE); + + dzl_fuzzy_index_query_async (index, "gtk", 0, NULL, + test_index_basic_query_cb, + g_object_ref (file)); +} + +static void +test_index_basic_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + DzlFuzzyIndexBuilder *builder = (DzlFuzzyIndexBuilder *)object; + g_autoptr(DzlFuzzyIndex) index = NULL; + g_autoptr(GFile) file = user_data; + GError *error = NULL; + gboolean r; + + r = dzl_fuzzy_index_builder_write_finish (builder, result, &error); + g_assert_no_error (error); + g_assert_cmpint (r, ==, TRUE); + + index = dzl_fuzzy_index_new (); + + dzl_fuzzy_index_load_file_async (index, + file, + NULL, + test_index_basic_load_cb, + g_object_ref (file)); +} + +static void +test_index_basic (void) +{ + DzlFuzzyIndexBuilder *builder; + GError *error = NULL; + GFile *file; + gboolean r; + + main_loop = g_main_loop_new (NULL, FALSE); + file = g_file_new_for_path ("index.gvariant"); + builder = dzl_fuzzy_index_builder_new (); + + /* + * We want to ensure we only get the highest scoring item for a + * document (which are deduplicated in the index). + */ + dzl_fuzzy_index_builder_insert (builder, "gtk_widget_show", g_variant_new_int32 (1), 7); + dzl_fuzzy_index_builder_insert (builder, "gtk_widget_show_all", g_variant_new_int32 (1), 7); + dzl_fuzzy_index_builder_insert (builder, "gtk_widget_hide", g_variant_new_int32 (2), 7); + dzl_fuzzy_index_builder_insert (builder, "gtk_widget_hide_all", g_variant_new_int32 (2), 7); + dzl_fuzzy_index_builder_insert (builder, "gtk_widget_get_parent", g_variant_new_int32 (3), 7); + dzl_fuzzy_index_builder_insert (builder, "gtk_widget_get_name", g_variant_new_int32 (4), 7); + dzl_fuzzy_index_builder_insert (builder, "gtk_widget_set_name", g_variant_new_int32 (5), 7); + + dzl_fuzzy_index_builder_write_async (builder, + file, + G_PRIORITY_LOW, + NULL, + test_index_basic_cb, + g_object_ref (file)); + + g_main_loop_run (main_loop); + g_assert_no_error (error); + + g_object_unref (builder); + + r = g_file_delete (file, NULL, &error); + g_assert_no_error (error); + g_assert (r); + + g_object_unref (file); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/Fuzzy/IndexBuilder/basic", test_index_builder_basic); + g_test_add_func ("/Dazzle/Fuzzy/Index/basic", test_index_basic); + return g_test_run (); +} diff --git a/tests/test-fuzzy-mutable-index.c b/tests/test-fuzzy-mutable-index.c new file mode 100644 index 0000000..4e6b610 --- /dev/null +++ b/tests/test-fuzzy-mutable-index.c @@ -0,0 +1,89 @@ +#include +#include + +static gint +compare_match (gconstpointer a, + gconstpointer b) +{ + const DzlFuzzyMutableIndexMatch *ma = a; + const DzlFuzzyMutableIndexMatch *mb = b; + + if (ma->score < mb->score) + return 1; + else if (ma->score > mb->score) + return -1; + return 0; +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_autofree gchar *path = g_build_filename (TEST_DATA_DIR, "test-fuzzy-mutable-index.txt", NULL); + DzlFuzzyMutableIndex *fuzzy; + GFileInputStream *file_stream; + GDataInputStream *data_stream; + gint64 begin; + gint64 end; + GFile *file; + gchar *line; + + if (argc == 1) + { + g_printerr ("usage: %s FUZZY_NEEDLE\n" + "Search fuzzy index for needle\n", argv[0]); + return EXIT_FAILURE; + } + + fuzzy = dzl_fuzzy_mutable_index_new_with_free_func (FALSE, g_free); + file = g_file_new_for_path (path); + file_stream = g_file_read (file, NULL, NULL); + data_stream = g_data_input_stream_new (G_INPUT_STREAM (file_stream)); + + dzl_fuzzy_mutable_index_begin_bulk_insert (fuzzy); + while (NULL != (line = g_data_input_stream_read_line (data_stream, NULL, NULL, NULL))) + dzl_fuzzy_mutable_index_insert (fuzzy, line, line); + dzl_fuzzy_mutable_index_end_bulk_insert (fuzzy); + + if (argc > 1) + { + guint i; + + for (i = 1; i < argc; i++) + { + const gchar *keyword; + GArray *matches; + + g_printerr ("Searching for \"%s\"\n", argv [i]); + + if (g_utf8_validate (argv[i], -1, NULL)) + keyword = argv[i]; + else + continue; + + begin = g_get_monotonic_time(); + matches = dzl_fuzzy_mutable_index_match (fuzzy, keyword, 0); + end = g_get_monotonic_time(); + + g_array_sort (matches, compare_match); + + for (guint j = 0; j < matches->len; j++) + { + const DzlFuzzyMutableIndexMatch *match = &g_array_index (matches, DzlFuzzyMutableIndexMatch, j); + g_print (" %0.4f : %s\n", match->score, match->key); + g_assert_cmpstr (match->key, ==, match->value); + } + + g_print ("Completed query in: %ld msec.\n", (glong)((end - begin) / 1000L)); + + g_array_unref (matches); + } + } + + dzl_fuzzy_mutable_index_unref (fuzzy); + g_clear_object (&file_stream); + g_clear_object (&data_stream); + g_clear_object (&file); + + return EXIT_SUCCESS; +} diff --git a/tests/test-heap.c b/tests/test-heap.c new file mode 100644 index 0000000..9c6c81f --- /dev/null +++ b/tests/test-heap.c @@ -0,0 +1,173 @@ +/* test-dzl-heap.c + * + * Copyright (C) 2014 Christian Hergert + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This file 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 + * Lesser 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 . + */ + +#include + +typedef struct +{ + gint64 size; + gpointer pointer; +} Tuple; + +static int +cmpint_rev (gconstpointer a, + gconstpointer b) +{ + return *(const gint *)b - *(const gint *)a; +} + +static int +cmpptr_rev (gconstpointer a, + gconstpointer b) +{ + return GPOINTER_TO_SIZE(*(gpointer *)b) - GPOINTER_TO_SIZE (*(gpointer *)a); +} + +static int +cmptuple_rev (gconstpointer a, + gconstpointer b) +{ + Tuple *at = (Tuple *)a; + Tuple *bt = (Tuple *)b; + + return bt->size - at->size; +} + +static void +test_DzlHeap_insert_val_int (void) +{ + DzlHeap *heap; + gint i; + + heap = dzl_heap_new (sizeof (gint), cmpint_rev); + + for (i = 0; i < 100000; i++) { + dzl_heap_insert_val (heap, i); + g_assert_cmpint (heap->len, ==, i + 1); + } + + for (i = 0; i < 100000; i++) { + g_assert_cmpint (heap->len, ==, 100000 - i); + g_assert_cmpint (dzl_heap_peek (heap, gint), ==, i); + dzl_heap_extract (heap, NULL); + } + + dzl_heap_unref (heap); +} + +static void +test_DzlHeap_insert_val_ptr (void) +{ + gconstpointer ptr; + DzlHeap *heap; + gint i; + + heap = dzl_heap_new (sizeof (gpointer), cmpptr_rev); + + for (i = 0; i < 100000; i++) { + ptr = GINT_TO_POINTER (i); + dzl_heap_insert_val (heap, ptr); + g_assert_cmpint (heap->len, ==, i + 1); + } + + for (i = 0; i < 100000; i++) { + g_assert_cmpint (heap->len, ==, 100000 - i); + g_assert (dzl_heap_peek (heap, gpointer) == GINT_TO_POINTER (i)); + dzl_heap_extract (heap, NULL); + } + + dzl_heap_unref (heap); +} + +static void +test_DzlHeap_insert_val_tuple (void) +{ + Tuple t; + DzlHeap *heap; + gint i; + + heap = dzl_heap_new (sizeof (Tuple), cmptuple_rev); + + for (i = 0; i < 100000; i++) { + t.pointer = GINT_TO_POINTER (i); + t.size = i; + dzl_heap_insert_val (heap, t); + g_assert_cmpint (heap->len, ==, i + 1); + } + + for (i = 0; i < 100000; i++) { + g_assert_cmpint (heap->len, ==, 100000 - i); + g_assert (dzl_heap_peek (heap, Tuple).size == i); + g_assert (dzl_heap_peek (heap, Tuple).pointer == GINT_TO_POINTER (i)); + dzl_heap_extract (heap, NULL); + } + + dzl_heap_unref (heap); +} + +static void +test_DzlHeap_extract_int (void) +{ + DzlHeap *heap; + gint removed[5]; + gint i; + gint v; + + heap = dzl_heap_new (sizeof (gint), cmpint_rev); + + for (i = 0; i < 100000; i++) { + dzl_heap_insert_val (heap, i); + } + + removed [0] = dzl_heap_index (heap, gint, 1578); dzl_heap_extract_index (heap, 1578, NULL); + removed [1] = dzl_heap_index (heap, gint, 2289); dzl_heap_extract_index (heap, 2289, NULL); + removed [2] = dzl_heap_index (heap, gint, 3312); dzl_heap_extract_index (heap, 3312, NULL); + removed [3] = dzl_heap_index (heap, gint, 78901); dzl_heap_extract_index (heap, 78901, NULL); + removed [4] = dzl_heap_index (heap, gint, 99000); dzl_heap_extract_index (heap, 99000, NULL); + + for (i = 0; i < 100000; i++) { + if (dzl_heap_peek (heap, gint) != i) { + g_assert ((i == removed[0]) || + (i == removed[1]) || + (i == removed[2]) || + (i == removed[3]) || + (i == removed[4])); + } else { + dzl_heap_extract (heap, &v); + g_assert_cmpint (v, ==, i); + } + } + + g_assert_cmpint (heap->len, ==, 0); + + dzl_heap_unref (heap); +} + +int +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/Dazzle/Heap/insert_and_extract", test_DzlHeap_insert_val_int); + g_test_add_func ("/Dazzle/Heap/insert_and_extract", test_DzlHeap_insert_val_ptr); + g_test_add_func ("/Dazzle/Heap/insert_and_extract", test_DzlHeap_insert_val_tuple); + g_test_add_func ("/Dazzle/Heap/extract_index", test_DzlHeap_extract_int); + + return g_test_run (); +} diff --git a/tests/test-int-pair.c b/tests/test-int-pair.c new file mode 100644 index 0000000..4643a05 --- /dev/null +++ b/tests/test-int-pair.c @@ -0,0 +1,79 @@ +#include + +static void +test_intpair_basic (void) +{ + DzlIntPair *p; + + p = dzl_int_pair_new (0, 0); +#ifdef DZL_INT_PAIR_64 + /* Technically not ANSI as NULL is allowed to be non-zero, but all the + * platforms we support, this is the case. + */ + g_assert (p == NULL); +#else + g_assert (p != NULL); +#endif + + p = dzl_int_pair_new (4, 5); + g_assert (p != NULL); + g_assert_cmpint (dzl_int_pair_first (p), ==, 4); + g_assert_cmpint (dzl_int_pair_second (p), ==, 5); + dzl_int_pair_free (p); + + p = dzl_int_pair_new (G_MAXINT, G_MAXINT-1); + g_assert (p != NULL); + g_assert_cmpint (dzl_int_pair_first (p), ==, G_MAXINT); + g_assert_cmpint (dzl_int_pair_second (p), ==, G_MAXINT-1); + dzl_int_pair_free (p); + + p = dzl_int_pair_new (G_MAXINT-1, G_MAXINT); + g_assert (p != NULL); + g_assert_cmpint (dzl_int_pair_first (p), ==, G_MAXINT-1); + g_assert_cmpint (dzl_int_pair_second (p), ==, G_MAXINT); + dzl_int_pair_free (p); +} + +static void +test_uintpair_basic (void) +{ + DzlUIntPair *p; + + p = dzl_uint_pair_new (0, 0); +#ifdef DZL_INT_PAIR_64 + /* Technically not ANSI as NULL is allowed to be non-zero, but all the + * platforms we support, this is the case. + */ + g_assert (p == NULL); +#else + g_assert (p != NULL); +#endif + + p = dzl_uint_pair_new (4, 5); + g_assert (p != NULL); + g_assert_cmpuint (dzl_uint_pair_first (p), ==, 4); + g_assert_cmpuint (dzl_uint_pair_second (p), ==, 5); + dzl_uint_pair_free (p); + + p = dzl_uint_pair_new (G_MAXUINT, G_MAXUINT-1); + g_assert (p != NULL); + g_assert_cmpuint (dzl_uint_pair_first (p), ==, G_MAXUINT); + g_assert_cmpuint (dzl_uint_pair_second (p), ==, G_MAXUINT-1); + dzl_uint_pair_free (p); + + p = dzl_uint_pair_new (G_MAXUINT-1, G_MAXUINT); + g_assert (p != NULL); + g_assert_cmpuint (dzl_uint_pair_first (p), ==, G_MAXUINT-1); + g_assert_cmpuint (dzl_uint_pair_second (p), ==, G_MAXUINT); + dzl_uint_pair_free (p); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/IntPair/basic", test_intpair_basic); + g_test_add_func ("/Dazzle/UIntPair/basic", test_uintpair_basic); + return g_test_run (); +} diff --git a/tests/test-joined-menu.c b/tests/test-joined-menu.c new file mode 100644 index 0000000..3f815ab --- /dev/null +++ b/tests/test-joined-menu.c @@ -0,0 +1,95 @@ +#include + +static void +load_css (void) +{ + g_autoptr(GtkCssProvider) provider = NULL; + + provider = dzl_css_provider_new ("resource:///org/gnome/dazzle/themes"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +gint +main (gint argc, + gchar *argv[]) +{ + GtkWidget *window; + GtkWidget *menu_button; + g_autoptr(DzlMenuManager) manager = NULL; + g_autoptr(DzlJoinedMenu) joined = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *filename1 = g_build_filename (TEST_DATA_DIR, "menus/joined1.ui", NULL); + g_autofree gchar *filename2 = g_build_filename (TEST_DATA_DIR, "menus/joined2.ui", NULL); + GMenu *menu1; + GMenu *menu2; + + gtk_init (&argc, &argv); + + load_css (); + + manager = dzl_menu_manager_new (); + + dzl_menu_manager_add_filename (manager, filename1, &error); + g_assert_no_error (error); + menu1 = dzl_menu_manager_get_menu_by_id (manager, "document-menu"); + + { + g_autofree gchar *icon = NULL; + g_autoptr(GMenuModel) section = NULL; + gboolean r; + + section = g_menu_model_get_item_link (G_MENU_MODEL (menu1), 0, "section"); + g_assert (section != NULL); + g_assert_cmpint (g_menu_model_get_n_items (section), ==, 3); + + r = g_menu_model_get_item_attribute (section, 0, "verb-icon-name", "s", &icon); + g_assert_true (r); + g_assert_cmpstr (icon, ==, "document-open-symbolic"); + } + + dzl_menu_manager_add_filename (manager, filename2, &error); + g_assert_no_error (error); + menu2 = dzl_menu_manager_get_menu_by_id (manager, "frame-menu"); + + joined = dzl_joined_menu_new (); + dzl_joined_menu_append_menu (joined, G_MENU_MODEL (menu1)); + dzl_joined_menu_append_menu (joined, G_MENU_MODEL (menu2)); + + { + g_autofree gchar *icon = NULL; + g_autoptr(GMenuModel) section = NULL; + gboolean r; + + section = g_menu_model_get_item_link (G_MENU_MODEL (joined), 0, "section"); + g_assert (section != NULL); + g_assert_cmpint (g_menu_model_get_n_items (section), ==, 3); + + r = g_menu_model_get_item_attribute (section, 0, "verb-icon-name", "s", &icon); + g_assert_true (r); + g_assert_cmpstr (icon, ==, "document-open-symbolic"); + } + + window = g_object_new (GTK_TYPE_WINDOW, + "default-width", 100, + "default-height", 100, + "border-width", 32, + "title", "Joined Menu Test", + NULL); + menu_button = g_object_new (DZL_TYPE_MENU_BUTTON, + "show-arrow", TRUE, + "show-accels", TRUE, + "show-icons", TRUE, + "icon-name", "document-open-symbolic", + "model", joined, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), menu_button); + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + + gtk_main (); + + return 0; +} diff --git a/tests/test-levenshtein.c b/tests/test-levenshtein.c new file mode 100644 index 0000000..82523b1 --- /dev/null +++ b/tests/test-levenshtein.c @@ -0,0 +1,39 @@ +#include + +typedef struct +{ + const gchar *needle; + const gchar *haystack; + gint expected_score; +} WordCheck; + +static void +test_levenshtein_basic (void) +{ + static const WordCheck check[] = { + { "gtk", "gkt", 2 }, + { "LibreFreeOpen", "Cromulent", 10 }, + { "Xorg", "Wayland", 7 }, + { "glib", "gobject", 6 }, + { "gbobject", "gobject", 1 }, + { "flip", "fliiiip", 3 }, + { "flip", "fliiiipper", 6 }, + { NULL } + }; + + for (guint i = 0; check[i].needle != NULL; i++) + { + gint score = dzl_levenshtein (check[i].needle, check[i].haystack); + + g_assert_cmpint (score, ==, check[i].expected_score); + } +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/Levenshtein/basic", test_levenshtein_basic); + return g_test_run (); +} diff --git a/tests/test-list-store-adapter.c b/tests/test-list-store-adapter.c new file mode 100644 index 0000000..278e85b --- /dev/null +++ b/tests/test-list-store-adapter.c @@ -0,0 +1,104 @@ +/* test-list-store-adapter.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#include + +static void +test_list_adapter (void) +{ + g_autoptr(GListStore) store = NULL; + g_autoptr(DzlListStoreAdapter) adapter = NULL; + GtkTreeIter iter; + gint n_children; + + store = g_list_store_new (G_TYPE_OBJECT); + adapter = dzl_list_store_adapter_new (G_LIST_MODEL (store)); + + n_children = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (adapter), NULL); + g_assert_cmpint (n_children, ==, 0); + + for (guint i = 0; i < 100; i++) + { + g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL); + g_object_set_data (obj, "ID", GINT_TO_POINTER (i)); + g_list_store_append (store, obj); + + n_children = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (adapter), NULL); + g_assert_cmpint (n_children, ==, i + 1); + } + + n_children = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (adapter), NULL); + g_assert_cmpint (n_children, ==, 100); + + for (gint i = 99; i > 0; i -= 2) + g_list_store_remove (store, i); + + n_children = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (adapter), NULL); + g_assert_cmpint (n_children, ==, 50); + + for (guint i = 0; i < 50; i++) + { + g_autoptr(GObject) obj = g_list_model_get_item (G_LIST_MODEL (store), i); + gint id = GPOINTER_TO_INT (g_object_get_data (obj, "ID")); + g_assert_cmpint (id, ==, i * 2); + } + + for (guint i = 10; i > 0; i--) + { + g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL); + g_object_set_data (obj, "ID", GINT_TO_POINTER (i - 1)); + g_list_store_insert (store, 20, obj); + } + + for (guint i = 0; i < 60; i++) + { + g_autoptr(GObject) obj = g_list_model_get_item (G_LIST_MODEL (store), i); + gint id = GPOINTER_TO_INT (g_object_get_data (obj, "ID")); + + if (i < 20) + g_assert_cmpint (id, ==, i * 2); + else if (i >= 30) + g_assert_cmpint (id, ==, (i-10) * 2); + else + g_assert_cmpint (id, ==, i - 20); + } + + gtk_tree_model_get_iter_first (GTK_TREE_MODEL (adapter), &iter); + + for (guint i = 0; i < 60; i++) + { + g_autoptr(GObject) obj = NULL; + g_autoptr(GObject) obj2 = NULL; + + obj = g_list_model_get_item (G_LIST_MODEL (store), i); + gtk_tree_model_get (GTK_TREE_MODEL (adapter), &iter, 0, &obj2, -1); + + g_assert (obj == obj2); + + gtk_tree_model_iter_next (GTK_TREE_MODEL (adapter), &iter); + } +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/ListStoreAdapter/basic", test_list_adapter); + return g_test_run (); +} diff --git a/tests/test-list-store.c b/tests/test-list-store.c new file mode 100644 index 0000000..1260317 --- /dev/null +++ b/tests/test-list-store.c @@ -0,0 +1,71 @@ +#include +#include + +static GRand *grand; + +static gint +compare_direct (gconstpointer a, + gconstpointer b, + gpointer data) +{ + if (a < b) + return -1; + else if (a > b) + return 1; + return 0; +} + +#define assert_cmpptr(a,eq,b) \ + G_STMT_START { \ + if (!((a) eq (b))) { \ + g_error (#a "(%p) " #eq " " #b "(%p)\n", (a), (b)); \ + } \ + } G_STMT_END + +static void +test_insert_sorted (void) +{ + GtkListStore *store = gtk_list_store_new (1, G_TYPE_POINTER); + GtkTreeIter iter; + gboolean r; + gpointer last = NULL; + guint count = 0; + + for (guint i = 0; i < 1000; i++) + { + gpointer value = GINT_TO_POINTER (g_rand_int (grand)); + + dzl_gtk_list_store_insert_sorted (store, &iter, value, 0, compare_direct, NULL); + gtk_list_store_set (store, &iter, 0, value, -1); + } + + r = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter); + g_assert_cmpint (r, ==, TRUE); + + do + { + gpointer value = NULL; + + gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 0, &value, -1); + assert_cmpptr (value, >=, last); + last = value; + + count++; + } + while (gtk_tree_model_iter_next (GTK_TREE_MODEL (store), &iter)); + + g_assert_cmpint (count, ==, 1000); + + g_object_unref (store); +} + +gint +main (gint argc, + gchar *argv[]) +{ + grand = g_rand_new (); + gtk_init (&argc, &argv); + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/GtkListStore/insert_sorted", test_insert_sorted); + return g_test_run (); +} diff --git a/tests/test-menu-manager.c b/tests/test-menu-manager.c new file mode 100644 index 0000000..ae3ecd4 --- /dev/null +++ b/tests/test-menu-manager.c @@ -0,0 +1,78 @@ +#include + +static const gchar * +build_path (const gchar *name) +{ + g_autofree gchar *path = NULL; + const gchar *ret; + + path = g_build_filename (TEST_DATA_DIR, "menus", name, NULL); + ret = g_intern_string (path); + + return ret; +} + +gint +main (gint argc, + gchar *argv[]) +{ + DzlMenuManager *manager; + GMenu *menu; + GtkWidget *widget; + GtkWindow *window; + GError *error = NULL; + GMenu *top; + + gtk_init (&argc, &argv); + + manager = dzl_menu_manager_new (); + + dzl_menu_manager_add_filename (manager, build_path ("menus.ui"), &error); + g_assert_no_error (error); + + dzl_menu_manager_add_filename (manager, build_path ("menus-exten-1.ui"), &error); + g_assert_no_error (error); + + dzl_menu_manager_add_filename (manager, build_path ("menus-exten-2.ui"), &error); + g_assert_no_error (error); + + dzl_menu_manager_add_filename (manager, build_path ("menus-exten-3.ui"), &error); + g_assert_no_error (error); + + dzl_menu_manager_add_filename (manager, build_path ("menus-exten-4.ui"), &error); + g_assert_no_error (error); + + dzl_menu_manager_add_filename (manager, build_path ("menus-exten-5.ui"), &error); + g_assert_no_error (error); + + top = g_menu_new (); + + menu = dzl_menu_manager_get_menu_by_id (manager, "menu-1"); + g_menu_append_submenu (top, "menu-1", G_MENU_MODEL (menu)); + + menu = dzl_menu_manager_get_menu_by_id (manager, "menu-2"); + g_menu_append_submenu (top, "menu-2", G_MENU_MODEL (menu)); + + menu = dzl_menu_manager_get_menu_by_id (manager, "menu-3"); + g_menu_append_submenu (top, "menu-3", G_MENU_MODEL (menu)); + + menu = dzl_menu_manager_get_menu_by_id (manager, "menu-4"); + g_menu_append_submenu (top, "menu-4", G_MENU_MODEL (menu)); + + window = g_object_new (GTK_TYPE_WINDOW, + "title", "Test Window", + NULL); + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + + widget = gtk_menu_bar_new_from_model (G_MENU_MODEL (top)); + gtk_widget_set_halign (widget, GTK_ALIGN_START); + gtk_widget_set_valign (widget, GTK_ALIGN_START); + gtk_container_add (GTK_CONTAINER (window), widget); + gtk_widget_show (widget); + + gtk_window_present (window); + + gtk_main (); + + return 0; +} diff --git a/tests/test-menu-manager2.c b/tests/test-menu-manager2.c new file mode 100644 index 0000000..07a6d20 --- /dev/null +++ b/tests/test-menu-manager2.c @@ -0,0 +1,70 @@ +#include + +#define assert_item_at_index(menu, i, label) \ +G_STMT_START { \ + g_autofree gchar *str = NULL; \ + gboolean r = g_menu_model_get_item_attribute (G_MENU_MODEL (menu), i, "label", "s", &str); \ + g_assert (r); \ + g_assert_cmpstr (str, ==, label); \ +} G_STMT_END + +static void +test_menu_manager (void) +{ + g_autoptr(DzlMenuManager) manager = dzl_menu_manager_new (); + g_autoptr(GMenu) menu1 = g_menu_new (); + g_autoptr(GMenu) menu2 = g_menu_new (); + g_autoptr(GMenuItem) item1 = g_menu_item_new ("item1", "item1"); + g_autoptr(GMenuItem) item2 = g_menu_item_new ("item2", "item2"); + g_autoptr(GMenuItem) item3 = g_menu_item_new ("item3", "item3"); + g_autoptr(GMenuItem) item4 = g_menu_item_new ("item4", "item4"); + g_autoptr(GMenuItem) item5 = g_menu_item_new ("item5", "item5"); + g_autoptr(GMenuItem) item6 = g_menu_item_new ("item6", "item6"); + GMenu *merged; + guint n_items; + + g_menu_item_set_attribute (item1, "after", "s", "item3"); + g_menu_item_set_attribute (item1, "before", "s", "item5"); + + g_menu_append_item (menu1, item1); + g_menu_append_item (menu1, item2); + g_menu_append_item (menu1, item3); + + dzl_menu_manager_merge (manager, "menu", G_MENU_MODEL (menu1)); + + merged = dzl_menu_manager_get_menu_by_id (manager, "menu"); + + assert_item_at_index (merged, 0, "item2"); + assert_item_at_index (merged, 1, "item3"); + assert_item_at_index (merged, 2, "item1"); + + g_menu_item_set_attribute (item4, "after", "s", "item2"); + g_menu_item_set_attribute (item4, "before", "s", "item3"); + + g_menu_append_item (menu2, item4); + g_menu_append_item (menu2, item5); + g_menu_append_item (menu2, item6); + + dzl_menu_manager_merge (manager, "menu", G_MENU_MODEL (menu2)); + + n_items = g_menu_model_get_n_items (G_MENU_MODEL (merged)); + g_assert_cmpint (n_items, ==, 6); + + assert_item_at_index (merged, 0, "item2"); + assert_item_at_index (merged, 1, "item4"); + assert_item_at_index (merged, 2, "item3"); + assert_item_at_index (merged, 3, "item1"); + assert_item_at_index (merged, 4, "item5"); + assert_item_at_index (merged, 5, "item6"); + + g_object_run_dispose (G_OBJECT (manager)); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/MenuManager/basic", test_menu_manager); + return g_test_run (); +} diff --git a/tests/test-multi-paned.c b/tests/test-multi-paned.c new file mode 100644 index 0000000..2174c3d --- /dev/null +++ b/tests/test-multi-paned.c @@ -0,0 +1,45 @@ +#include + +static void +swap_orientation (GtkWidget *button, + GtkWidget *paned) +{ + GtkOrientation orientation; + + orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (paned)); + gtk_orientable_set_orientation (GTK_ORIENTABLE (paned), !orientation); +} + +gint +main (gint argc, + gchar *argv[]) +{ + GtkBuilder *builder = NULL; + GtkWindow *window = NULL; + GError *error = NULL; + GObject *button; + GObject *paned; + g_autofree gchar *path = g_build_filename (TEST_DATA_DIR, "test-multi-paned.ui", NULL); + + gtk_init (&argc, &argv); + + g_type_ensure (DZL_TYPE_MULTI_PANED); + + builder = gtk_builder_new (); + gtk_builder_add_from_file (builder, path, &error); + g_assert_no_error (error); + + paned = gtk_builder_get_object (builder, "paned"); + button = gtk_builder_get_object (builder, "swap"); + g_signal_connect (button, "clicked", G_CALLBACK (swap_orientation), paned); + + window = GTK_WINDOW (gtk_builder_get_object (builder, "window")); + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + + gtk_window_present (window); + gtk_main (); + + g_clear_object (&builder); + + return 0; +} diff --git a/tests/test-panel.c b/tests/test-panel.c new file mode 100644 index 0000000..c6b54a9 --- /dev/null +++ b/tests/test-panel.c @@ -0,0 +1,213 @@ +#include + +typedef enum +{ + COMMAND_SHOW, + COMMAND_HIDE, + COMMAND_EXIT +} Command; + +#define INTERVAL 500 + +static struct { + gint interval; + Command command; +} commands[] = { + { 1000, COMMAND_SHOW }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { INTERVAL, COMMAND_SHOW }, + { INTERVAL, COMMAND_HIDE }, + { 300, COMMAND_EXIT }, +}; +static gint current_command; +static GtkWidget *dockbin; +static GtkWidget *class_entry; +static GTimer *timer; + +static void +toggle_all (gboolean show) +{ + DzlDockRevealer *edge; + DzlDockBin *dock = DZL_DOCK_BIN (dockbin); + + edge = DZL_DOCK_REVEALER (dzl_dock_bin_get_left_edge (dock)); + dzl_dock_revealer_set_reveal_child (edge, show); + + edge = DZL_DOCK_REVEALER (dzl_dock_bin_get_right_edge (dock)); + dzl_dock_revealer_set_reveal_child (edge, show); + + edge = DZL_DOCK_REVEALER (dzl_dock_bin_get_bottom_edge (dock)); + dzl_dock_revealer_set_reveal_child (edge, show); +} + +static void +adjust_sizes (void) +{ + DzlDockBin *dock = DZL_DOCK_BIN (dockbin); + GtkWidget *edge; + + edge = dzl_dock_bin_get_left_edge (dock); + dzl_dock_revealer_set_position (DZL_DOCK_REVEALER (edge), 300); + + edge = dzl_dock_bin_get_right_edge (dock); + dzl_dock_revealer_set_position (DZL_DOCK_REVEALER (edge), 300); + + edge = dzl_dock_bin_get_bottom_edge (dock); + dzl_dock_revealer_set_position (DZL_DOCK_REVEALER (edge), 300); +} + +static void +grab_class_entry (GtkWidget *button) +{ + gtk_widget_grab_focus (class_entry); +} + +static gboolean +process_command (gpointer data) +{ + switch (commands [current_command].command) + { + case COMMAND_SHOW: + toggle_all (TRUE); + break; + + case COMMAND_HIDE: + toggle_all (FALSE); + break; + + case COMMAND_EXIT: + default: + gtk_main_quit (); + return G_SOURCE_REMOVE; + } + + g_timeout_add (commands [++current_command].interval, + process_command, + NULL); + + return G_SOURCE_REMOVE; +} + +static void +log_handler (const gchar *domain, + GLogLevelFlags flags, + const gchar *message, + gpointer user_data) +{ + gdouble t = g_timer_elapsed (timer, NULL); + + g_print ("%s: time=%0.5lf %s\n", domain, t, message); +} + +static void +load_css (void) +{ + g_autoptr(GtkCssProvider) provider = NULL; + + provider = dzl_css_provider_new ("resource:///org/gnome/dazzle/themes"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +gint +main (gint argc, + gchar *argv[]) +{ + GtkBuilder *builder = NULL; + GtkWindow *window = NULL; + GActionGroup *group; + GError *error = NULL; + g_autofree gchar *ui_path = g_build_filename (TEST_DATA_DIR, "test-panel.ui", NULL); + + gtk_init (&argc, &argv); + + load_css (); + + timer = g_timer_new (); + + builder = gtk_builder_new (); + gtk_builder_add_from_file (builder, ui_path, &error); + g_assert_no_error (error); + + window = GTK_WINDOW (gtk_builder_get_object (builder, "window")); + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + + dockbin = GTK_WIDGET (gtk_builder_get_object (builder, "dockbin")); + /* _dzl_dock_item_printf (DZL_DOCK_ITEM (dockbin)); */ + + group = gtk_widget_get_action_group (dockbin, "dockbin"); + gtk_widget_insert_action_group (GTK_WIDGET (window), "dockbin", group); + + adjust_sizes (); + + class_entry = GTK_WIDGET (gtk_builder_get_object (builder, "class_entry")); + +#if 0 + gtk_builder_add_callback_symbol (builder, "toggle_all", G_CALLBACK (toggle_all)); +#endif + gtk_builder_add_callback_symbol (builder, "grab_class_entry", G_CALLBACK (grab_class_entry)); + gtk_builder_connect_signals (builder, dockbin); + + if (0) + g_timeout_add (commands [0].interval, + process_command, + NULL); + + g_log_set_default_handler (log_handler, NULL); + + gtk_widget_grab_focus (GTK_WIDGET (gtk_builder_get_object (builder, "text_view"))); + + gtk_window_maximize (window); + gtk_window_present (window); + gtk_main (); + + g_clear_object (&builder); + + return 0; +} diff --git a/tests/test-path-bar.c b/tests/test-path-bar.c new file mode 100644 index 0000000..4166a1d --- /dev/null +++ b/tests/test-path-bar.c @@ -0,0 +1,107 @@ +#include + +static void +load_css (void) +{ + g_autoptr(GtkCssProvider) provider = NULL; + + provider = dzl_css_provider_new ("resource:///org/gnome/dazzle/themes"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +gint +main (gint argc, + gchar *argv[]) +{ + GtkWidget *window; + GtkWidget *headerbar; + GtkWidget *pathbar; + DzlPath *path; + DzlPathElement *ele0; + DzlPathElement *ele1; + DzlPathElement *ele2; + DzlPathElement *ele3; + + gtk_init (&argc, &argv); + + load_css (); + + window = g_object_new (GTK_TYPE_WINDOW, + "default-width", 600, + "default-height", 200, + "title", "Test Path Bar", + NULL); + headerbar = g_object_new (GTK_TYPE_HEADER_BAR, + "show-close-button", TRUE, + "visible", TRUE, + NULL); + gtk_window_set_titlebar (GTK_WINDOW (window), headerbar); + + pathbar = g_object_new (DZL_TYPE_PATH_BAR, + "hexpand", TRUE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (headerbar), pathbar); + + path = dzl_path_new (); + g_assert (dzl_path_is_empty (path)); + g_assert_cmpint (dzl_path_get_length (path), ==, 0); + g_assert (NULL == dzl_path_get_elements (path)); + + ele1 = dzl_path_element_new ("home", "user-home-symbolic", "Home"); + dzl_path_append (path, ele1); + g_assert (NULL != dzl_path_get_elements (path)); + g_assert (dzl_path_get_elements (path)->data == ele1); + + ele2 = dzl_path_element_new ("christian", NULL, "Xtian"); + dzl_path_append (path, ele2); + g_assert (dzl_path_get_elements (path)->next->data == ele2); + g_assert (dzl_path_get_elements (path)->next->next == NULL); + + ele3 = dzl_path_element_new ("music", NULL, "Music"); + dzl_path_append (path, ele3); + g_assert (dzl_path_get_elements (path)->data == ele1); + g_assert (dzl_path_get_elements (path)->next->data == ele2); + g_assert (dzl_path_get_elements (path)->next->next->data == ele3); + g_assert (dzl_path_get_elements (path)->next->next->next == NULL); + + ele0 = dzl_path_element_new ("computer", "computer-symbolic", "Computer"); + dzl_path_prepend (path, ele0); + g_assert (dzl_path_get_elements (path)->data == ele0); + g_assert (dzl_path_get_elements (path)->next->data == ele1); + + dzl_path_bar_set_path (DZL_PATH_BAR (pathbar), path); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + + g_object_add_weak_pointer (G_OBJECT (pathbar), (gpointer *)&pathbar); + + gtk_main (); + + g_object_add_weak_pointer (G_OBJECT (path), (gpointer *)&path); + g_object_add_weak_pointer (G_OBJECT (ele0), (gpointer *)&ele0); + g_object_add_weak_pointer (G_OBJECT (ele1), (gpointer *)&ele1); + g_object_add_weak_pointer (G_OBJECT (ele2), (gpointer *)&ele2); + g_object_add_weak_pointer (G_OBJECT (ele3), (gpointer *)&ele3); + + /* ensure we clear path references */ + if (pathbar) + dzl_path_bar_set_path (DZL_PATH_BAR (pathbar), NULL); + + g_object_unref (path); + g_object_unref (ele0); + g_object_unref (ele1); + g_object_unref (ele2); + g_object_unref (ele3); + + g_assert (path == NULL); + g_assert (ele0 == NULL); + g_assert (ele1 == NULL); + g_assert (ele2 == NULL); + g_assert (ele3 == NULL); + + return 0; +} diff --git a/tests/test-pattern-spec.c b/tests/test-pattern-spec.c new file mode 100644 index 0000000..fdb4724 --- /dev/null +++ b/tests/test-pattern-spec.c @@ -0,0 +1,56 @@ +/* test-pattern-spec.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#include + +static void +test_basic (void) +{ + static const struct { + const gchar *needle; + const gchar *haystack; + gboolean result; + } tests[] = { + { "blue", "red blue purple pink green black cyan", TRUE }, + { "Blue", "red blue purple pink green black cyan", FALSE }, + { "blue", "red Blue purple pink green black cyan", TRUE }, + { "DzlPatternSpec", "DzlPatternSpec_autoptr", TRUE }, + { "dzlpattern", "DzlPatternSpec_autoptr", TRUE }, + { "dzlpattern", "dzl_pattern_spec", FALSE }, + { "dzl pattern", "dzl_pattern_spec", TRUE }, + }; + + for (guint i = 0; i < G_N_ELEMENTS (tests); i++) + { + g_autoptr(DzlPatternSpec) spec = NULL; + gboolean r; + + spec = dzl_pattern_spec_new (tests[i].needle); + r = dzl_pattern_spec_match (spec, tests[i].haystack); + g_assert_cmpint (r, ==, tests[i].result); + } +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/PatternSpec/basic", test_basic); + return g_test_run (); +} diff --git a/tests/test-pill-box.c b/tests/test-pill-box.c new file mode 100644 index 0000000..1330041 --- /dev/null +++ b/tests/test-pill-box.c @@ -0,0 +1,61 @@ +#include + +static const gchar *words[] = { + "Autotools", + "Meson", + "CMake", + "waf", + "scons", + "shell", + NULL +}; + +static void +load_css (void) +{ + g_autoptr(GtkCssProvider) provider = NULL; + + provider = dzl_css_provider_new ("resource:///org/gnome/dazzle/themes"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +int +main (int argc, + char *argv[]) +{ + GtkWidget *window; + GtkWidget *box; + GtkWidget *pill; + + gtk_init (&argc, &argv); + + load_css (); + + window = g_object_new (GTK_TYPE_WINDOW, + "title", "Test PillBox", + "border-width", 24, + NULL); + box = g_object_new (GTK_TYPE_BOX, + "spacing", 3, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), box); + + for (guint i = 0; words[i]; i++) + { + pill = g_object_new (DZL_TYPE_PILL_BOX, + "label", words[i], + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (box), pill); + } + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + + gtk_main (); + + return 0; +} diff --git a/tests/test-preferences.c b/tests/test-preferences.c new file mode 100644 index 0000000..550f016 --- /dev/null +++ b/tests/test-preferences.c @@ -0,0 +1,90 @@ +#include + +static const gchar *themes[] = { + "Applications", + "Cursor", + "Icons", + "System", + NULL +}; + +static void +add_preferences (DzlPreferences *prefs) +{ + dzl_preferences_add_page (prefs, "appearance", "Appearance", 0); + dzl_preferences_add_page (prefs, "desktop", "Desktop", 1); + dzl_preferences_add_page (prefs, "extensions", "Extensions", 2); + dzl_preferences_add_page (prefs, "fonts", "Fonts", 3); + dzl_preferences_add_page (prefs, "keyboard", "Keyboard & Mouse", 4); + dzl_preferences_add_page (prefs, "power", "Power", 5); + dzl_preferences_add_page (prefs, "startup", "Startup Applications", 6); + dzl_preferences_add_page (prefs, "topbar", "Top Bar", 7); + dzl_preferences_add_page (prefs, "windows", "Windows", 7); + dzl_preferences_add_page (prefs, "workspaces", "Workspaces", 8); + + dzl_preferences_add_group (prefs, "appearance", "basic", NULL, 0); + dzl_preferences_add_switch (prefs, "appearance", "basic", "com.example", "foo", NULL, NULL, "Global Dark Theme", "Applications need to be restarted for this change to take place", "dark theme", 0); + dzl_preferences_add_switch (prefs, "appearance", "basic", "com.example", "foo", NULL, NULL, "Animations", NULL, "animations", 1); + + dzl_preferences_add_list_group (prefs, "appearance", "themes", "Themes", GTK_SELECTION_NONE, 10); + + for (guint i = 0; themes[i]; i++) + dzl_preferences_add_custom (prefs, "appearance", "themes", + g_object_new (GTK_TYPE_LABEL, + "label", themes[i], + "visible", TRUE, + "xalign", 0.0f, + NULL), + themes[i], i); + + dzl_preferences_add_group (prefs, "appearance", "install", NULL, 20); + dzl_preferences_add_custom (prefs, "appearance", "install", + g_object_new (GTK_TYPE_BUTTON, + "label", "Install from File…", + "halign", GTK_ALIGN_END, + "visible", TRUE, + NULL), + NULL, 0); + + dzl_preferences_set_page (prefs, "appearance", NULL); +} + +static void +load_css (void) +{ + g_autoptr(GtkCssProvider) provider = NULL; + + provider = dzl_css_provider_new ("resource:///org/gnome/dazzle/themes"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +gint +main (gint argc, + gchar *argv[]) +{ + GtkWidget *window; + GtkWidget *view; + + gtk_init (&argc, &argv); + + load_css (); + + window = g_object_new (GTK_TYPE_WINDOW, + "title", "Preferences Test", + "default-width", 800, + "default-height", 600, + NULL); + + view = g_object_new (DZL_TYPE_PREFERENCES_VIEW, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), view); + + add_preferences (DZL_PREFERENCES (view)); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + return gtk_main (), 0; +} diff --git a/tests/test-progress-button.c b/tests/test-progress-button.c new file mode 100644 index 0000000..74c9720 --- /dev/null +++ b/tests/test-progress-button.c @@ -0,0 +1,48 @@ +#include + +static void +load_css (void) +{ + g_autoptr(GtkCssProvider) provider = NULL; + + provider = dzl_css_provider_new ("resource:///org/gnome/dazzle/themes"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +gint +main (gint argc, + gchar *argv[]) +{ + GtkWidget *window; + GtkWidget *button; + + gtk_init (&argc, &argv); + + load_css (); + + window = g_object_new (GTK_TYPE_WINDOW, + "border-width", 24, + "title", "Progress Button Test", + "visible", TRUE, + NULL); + + button = g_object_new (DZL_TYPE_PROGRESS_BUTTON, + "label", "Downloading…", + "progress", 0, + "show-progress", TRUE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), button); + + dzl_object_animate (button, DZL_ANIMATION_LINEAR, 5000, NULL, + "progress", 100, + NULL); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + gtk_main (); + + return 0; +} diff --git a/tests/test-progress-icon.c b/tests/test-progress-icon.c new file mode 100644 index 0000000..6550288 --- /dev/null +++ b/tests/test-progress-icon.c @@ -0,0 +1,35 @@ +#include + +gint +main (gint argc, + gchar *argv[]) +{ + GtkWidget *window; + GtkWidget *button; + + gtk_init (&argc, &argv); + + window = g_object_new (GTK_TYPE_WINDOW, + "border-width", 24, + "title", "Progress Icon Test", + "visible", TRUE, + NULL); + + button = g_object_new (DZL_TYPE_PROGRESS_ICON, + "progress", 0.0, + "visible", TRUE, + "width-request", 96, + "height-request", 96, + NULL); + gtk_container_add (GTK_CONTAINER (window), button); + + dzl_object_animate (button, DZL_ANIMATION_LINEAR, 5000, NULL, + "progress", 1.0, + NULL); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + gtk_main (); + + return 0; +} diff --git a/tests/test-progress-menu-button.c b/tests/test-progress-menu-button.c new file mode 100644 index 0000000..e8d6875 --- /dev/null +++ b/tests/test-progress-menu-button.c @@ -0,0 +1,34 @@ +#include + +gint +main (gint argc, + gchar *argv[]) +{ + GtkWidget *window; + GtkWidget *button; + + gtk_init (&argc, &argv); + + window = g_object_new (GTK_TYPE_WINDOW, + "border-width", 24, + "title", "Progress Button Test", + "visible", TRUE, + NULL); + + button = g_object_new (DZL_TYPE_PROGRESS_MENU_BUTTON, + "halign", GTK_ALIGN_CENTER, + "valign", GTK_ALIGN_CENTER, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), button); + + dzl_object_animate (button, DZL_ANIMATION_LINEAR, 5000, NULL, + "progress", 1.0, + NULL); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + gtk_main (); + + return 0; +} diff --git a/tests/test-radio-box.c b/tests/test-radio-box.c new file mode 100644 index 0000000..a96950b --- /dev/null +++ b/tests/test-radio-box.c @@ -0,0 +1,34 @@ +#include +#include + +gint +main (gint argc, + gchar *argv[]) +{ + GtkWindow *window; + DzlRadioBox *box; + + gtk_init (&argc, &argv); + + window = g_object_new (GTK_TYPE_WINDOW, + "title", "Test DzlRadioBox", + NULL); + box = g_object_new (DZL_TYPE_RADIO_BOX, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (box)); + + dzl_radio_box_add_item (box, "1", "One"); + dzl_radio_box_add_item (box, "2", "Two"); + dzl_radio_box_add_item (box, "3", "Three"); + dzl_radio_box_add_item (box, "4", "Four"); + dzl_radio_box_add_item (box, "5", "Five"); + dzl_radio_box_add_item (box, "6", "Six"); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (window); + + gtk_main (); + + return EXIT_SUCCESS; +} diff --git a/tests/test-recursive-monitor.c b/tests/test-recursive-monitor.c new file mode 100644 index 0000000..f2e187d --- /dev/null +++ b/tests/test-recursive-monitor.c @@ -0,0 +1,209 @@ +#include +#include + +static const gchar *layer1[] = { "a", "b", "c", "d", "e", "f", "g", "h", "i", NULL }; +static const gchar *layer2[] = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", NULL }; + +enum { + MODE_CREATE, + MODE_DELETE, +}; + +typedef struct +{ + GMainLoop *main_loop; + DzlRecursiveFileMonitor *monitor; + GHashTable *created; + GHashTable *deleted; + GQueue dirs; + GList *iter; + guint mode : 1; + guint did_action : 1; +} BasicState; + +static gboolean +failed_timeout (gpointer state) +{ + /* timed out */ + g_assert_not_reached (); + return G_SOURCE_REMOVE; +} + +static gboolean +begin_test_basic (gpointer data) +{ + g_autoptr(GError) error = NULL; + BasicState *state = data; + GFile *current; + gboolean r; + + g_assert (state != NULL); + g_assert (state->iter != NULL); + g_assert (state->iter->data != NULL); + g_assert (G_IS_FILE (state->iter->data)); + + current = state->iter->data; + + if (state->mode == MODE_CREATE) + { + if (g_hash_table_contains (state->created, current)) + { + state->iter = state->iter->next; + state->did_action = FALSE; + + if (state->iter == NULL) + { + state->mode = MODE_DELETE; + state->iter = state->dirs.tail; + } + } + else if (!state->did_action) + { + state->did_action = TRUE; + r = g_file_make_directory (current, NULL, &error); + g_assert_no_error (error); + g_assert_cmpint (r, ==, TRUE); + } + } + else if (state->mode == MODE_DELETE) + { + if (g_hash_table_contains (state->deleted, current)) + { + state->iter = state->iter->prev; + state->did_action = FALSE; + + if (state->iter == NULL) + { + g_main_loop_quit (state->main_loop); + return G_SOURCE_REMOVE; + } + } + else if (!state->did_action) + { + state->did_action = TRUE; + g_file_delete (current, NULL, &error); + g_assert_no_error (error); + } + } + else + g_assert_not_reached (); + + return G_SOURCE_CONTINUE; +} + +static void +monitor_changed_cb (DzlRecursiveFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event, + gpointer data) +{ + BasicState *state = data; + + if (event == G_FILE_MONITOR_EVENT_CREATED) + g_hash_table_insert (state->created, g_object_ref (file), NULL); + else if (event == G_FILE_MONITOR_EVENT_DELETED) + g_hash_table_insert (state->deleted, g_object_ref (file), NULL); +} + +static void +started_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + DzlRecursiveFileMonitor *monitor = (DzlRecursiveFileMonitor *)object; + BasicState *state = user_data; + g_autoptr(GError) error = NULL; + gboolean r; + + g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (monitor)); + + r = dzl_recursive_file_monitor_start_finish (monitor, result, &error); + g_assert_no_error (error); + g_assert_cmpint (r, ==, TRUE); + + /* + * Now start the async test processing. We use a very + * low priority idle to ensure other things process before us. + */ + + state->iter = state->dirs.head; + state->did_action = FALSE; + state->mode = MODE_CREATE; + g_idle_add_full (G_MAXINT, begin_test_basic, state, NULL); +} + +static void +test_basic (void) +{ + g_autoptr(GFile) dir = g_file_new_for_path ("recursive-dir"); + BasicState state = { 0 }; + gint r; + + state.main_loop = g_main_loop_new (NULL, FALSE); + g_queue_init (&state.dirs); + state.created = g_hash_table_new_full (g_file_hash, + (GEqualFunc) g_file_equal, + g_object_unref, + NULL); + state.deleted = g_hash_table_new_full (g_file_hash, + (GEqualFunc) g_file_equal, + g_object_unref, + NULL); + + /* Cleanup any previously failed run */ + if (g_file_test ("recursive-dir", G_FILE_TEST_EXISTS)) + { + g_autoptr(DzlDirectoryReaper) reaper = dzl_directory_reaper_new (); + + dzl_directory_reaper_add_directory (reaper, dir, 0); + dzl_directory_reaper_execute (reaper, NULL, NULL); + + r = g_rmdir ("recursive-dir"); + g_assert_cmpint (r, ==, 0); + } + + /* Create our root directory to use */ + r = g_mkdir ("recursive-dir", 0750); + g_assert_cmpint (r, ==, 0); + + /* Build our list of directories to create/test */ + for (guint i = 0; layer1[i]; i++) + { + g_autoptr(GFile) file1 = g_file_new_build_filename ("recursive-dir", layer1[i], NULL); + + g_queue_push_tail (&state.dirs, g_object_ref (file1)); + + for (guint j = 0; layer2[j]; j++) + { + g_autoptr(GFile) file2 = g_file_get_child (file1, layer2[j]); + g_queue_push_tail (&state.dirs, g_steal_pointer (&file2)); + } + } + + state.monitor = dzl_recursive_file_monitor_new (dir); + g_signal_connect (state.monitor, "changed", G_CALLBACK (monitor_changed_cb), &state); + + /* Add a timeout to avoid infinite running */ + g_timeout_add_seconds (3, failed_timeout, &state); + + dzl_recursive_file_monitor_start_async (state.monitor, NULL, started_cb, &state); + + g_main_loop_run (state.main_loop); + + dzl_recursive_file_monitor_cancel (state.monitor); + + g_clear_pointer (&state.main_loop, g_main_loop_unref); + g_clear_pointer (&state.created, g_hash_table_unref); + g_clear_pointer (&state.deleted, g_hash_table_unref); + g_clear_object (&state.monitor); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/RecursiveFileMonitor/basic", test_basic); + return g_test_run (); +} diff --git a/tests/test-ring.c b/tests/test-ring.c new file mode 100644 index 0000000..4d469a1 --- /dev/null +++ b/tests/test-ring.c @@ -0,0 +1,250 @@ +/* test-ring.c + * + * Copyright (C) 2010-2017 Christian Hergert + * + * 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 . + */ + +#include + +static void +test_index_conversion (void) +{ + g_autoptr(DzlRing) ring = dzl_ring_sized_new (sizeof (guint), 10, NULL); + + g_assert_cmpint (ring->len, ==, 10); + g_assert_cmpint (ring->pos, ==, 0); + + g_assert_cmpint (0, ==, _dzl_ring_index (ring, 0)); + g_assert_cmpint (1, ==, _dzl_ring_index (ring, 1)); + g_assert_cmpint (2, ==, _dzl_ring_index (ring, 2)); + g_assert_cmpint (3, ==, _dzl_ring_index (ring, 3)); + g_assert_cmpint (4, ==, _dzl_ring_index (ring, 4)); + g_assert_cmpint (5, ==, _dzl_ring_index (ring, 5)); + g_assert_cmpint (6, ==, _dzl_ring_index (ring, 6)); + g_assert_cmpint (7, ==, _dzl_ring_index (ring, 7)); + g_assert_cmpint (8, ==, _dzl_ring_index (ring, 8)); + g_assert_cmpint (9, ==, _dzl_ring_index (ring, 9)); + g_assert_cmpint (0, ==, _dzl_ring_index (ring, 10)); + g_assert_cmpint (1, ==, _dzl_ring_index (ring, 11)); + g_assert_cmpint (9, ==, _dzl_ring_index (ring, -1)); + g_assert_cmpint (8, ==, _dzl_ring_index (ring, -2)); + g_assert_cmpint (7, ==, _dzl_ring_index (ring, -3)); + g_assert_cmpint (6, ==, _dzl_ring_index (ring, -4)); + g_assert_cmpint (5, ==, _dzl_ring_index (ring, -5)); + g_assert_cmpint (4, ==, _dzl_ring_index (ring, -6)); + g_assert_cmpint (3, ==, _dzl_ring_index (ring, -7)); + g_assert_cmpint (2, ==, _dzl_ring_index (ring, -8)); + g_assert_cmpint (1, ==, _dzl_ring_index (ring, -9)); + g_assert_cmpint (0, ==, _dzl_ring_index (ring, -10)); +} + +static void +test_DzlRing_ref (void) +{ + DzlRing *ring; + + ring = dzl_ring_sized_new (sizeof (gdouble), 60, NULL); + g_assert (ring); + dzl_ring_unref (ring); +} + +static void +test_various_lengths (gint len) +{ + DzlRing *ring; + gdouble d; + + ring = dzl_ring_sized_new (sizeof (gdouble), len, NULL); + g_assert (ring); + + d = 3.; + dzl_ring_append_val (ring, d); + d = dzl_ring_get_index (ring, gdouble, -1); + g_assert_cmpint (d, ==, 3.); + d = dzl_ring_get_index (ring, gdouble, len-1); + g_assert_cmpint (d, ==, 3.); + d = dzl_ring_get_index (ring, gdouble, 0); + g_assert_cmpint (d, ==, 0.); + + d = 4.; + dzl_ring_append_val (ring, d); + d = dzl_ring_get_index (ring, gdouble, -1); + g_assert_cmpint (d, ==, 4.); + d = dzl_ring_get_index (ring, gdouble, len-1); + g_assert_cmpint (d, ==, 4.); + d = dzl_ring_get_index (ring, gdouble, -2); + g_assert_cmpint (d, ==, 3.); + d = dzl_ring_get_index (ring, gdouble, len-2); + g_assert_cmpint (d, ==, 3.); + d = dzl_ring_get_index (ring, gdouble, 0); + g_assert_cmpint (d, ==, 0.); + + d = 6.; + dzl_ring_append_val (ring, d); + d = dzl_ring_get_index (ring, gdouble, -1); + g_assert_cmpint (d, ==, 6.); + d = dzl_ring_get_index (ring, gdouble, len-1); + g_assert_cmpint (d, ==, 6.); + d = dzl_ring_get_index (ring, gdouble, -2); + g_assert_cmpint (d, ==, 4.); + d = dzl_ring_get_index (ring, gdouble, len-2); + g_assert_cmpint (d, ==, 4.); + d = dzl_ring_get_index (ring, gdouble, -3); + g_assert_cmpint (d, ==, 3.); + d = dzl_ring_get_index (ring, gdouble, len-3); + g_assert_cmpint (d, ==, 3.); + d = dzl_ring_get_index (ring, gdouble, -4); + g_assert_cmpint (d, ==, 0.); + d = dzl_ring_get_index (ring, gdouble, len-4); + g_assert_cmpint (d, ==, 0.); + d = dzl_ring_get_index (ring, gdouble, 0); + g_assert_cmpint (d, ==, 0.); + d = dzl_ring_get_index (ring, gdouble, len); + g_assert_cmpint (d, ==, 0.); + + dzl_ring_unref (ring); +} + +static void +test_DzlRing_with_double (void) +{ + for (guint i = 20; i < 100; i++) + test_various_lengths (i); +} + +static void +test_DzlRing_with_int (void) +{ + DzlRing *ring; + gint i; + + ring = dzl_ring_sized_new (sizeof (gint), 10, NULL); + i = dzl_ring_get_index (ring, gint, 0); + g_assert_cmpint (i, ==, 0); + i = 10; + dzl_ring_append_val (ring, i); + i = dzl_ring_get_index (ring, gint, -1); + g_assert_cmpint (i, ==, 10); + i = 20; + dzl_ring_append_val (ring, i); + i = dzl_ring_get_index (ring, gint, -1); + g_assert_cmpint (i, ==, 20); + i = dzl_ring_get_index (ring, gint, -2); + g_assert_cmpint (i, ==, 10); + i = dzl_ring_get_index (ring, gint, 0); + g_assert_cmpint (i, ==, 0); + dzl_ring_unref (ring); +} + +static gboolean test_DzlRing_with_array_result = FALSE; + +static void +test_DzlRing_with_array_cb (gpointer data) +{ + GArray **ar = data; + test_DzlRing_with_array_result = TRUE; + g_array_unref (*ar); +} + +static void +test_DzlRing_with_array (void) +{ + DzlRing *ring; + GArray *ar0 = NULL; + GArray *ar1 = NULL; + GArray *ar2 = NULL; + gpointer tmp; + + ring = dzl_ring_sized_new (sizeof (GArray*), 2, test_DzlRing_with_array_cb); + ar0 = g_array_new (FALSE, TRUE, sizeof (gdouble)); + ar1 = g_array_new (FALSE, TRUE, sizeof (gdouble)); + ar2 = g_array_new (FALSE, TRUE, sizeof (gdouble)); + + dzl_ring_append_val (ring, ar0); + dzl_ring_append_val (ring, ar1); + tmp = dzl_ring_get_index (ring, GArray*, -1); + g_assert (tmp == ar1); + tmp = dzl_ring_get_index (ring, GArray*, -2); + g_assert (tmp == ar0); + + /* + * Make sure that ar0 was dispoased as we ran over it. + */ + dzl_ring_append_val (ring, ar2); + tmp = dzl_ring_get_index (ring, GArray*, -1); + g_assert (tmp == ar2); + tmp = dzl_ring_get_index (ring, GArray*, -2); + g_assert (tmp == ar1); + g_assert_cmpint (test_DzlRing_with_array_result, ==, TRUE); + + dzl_ring_unref (ring); +} + +static void +test_DzlRing_foreach_cb (gpointer data, + gpointer user_data) +{ + gboolean *success = user_data; + *success = FALSE; +} + +static void +test_DzlRing_foreach_cb2 (gpointer data, + gpointer user_data) +{ + gboolean *success = user_data; + gpointer *p = data; + + if (*p) + { + g_assert (*p == GINT_TO_POINTER (1234)); + *success = TRUE; + } +} + +static void +test_DzlRing_foreach (void) +{ + DzlRing *ring; + gboolean success = TRUE; + gpointer data = GINT_TO_POINTER (1234); + + ring = dzl_ring_sized_new (sizeof (gpointer), 20, NULL); + dzl_ring_foreach (ring, test_DzlRing_foreach_cb, &success); + g_assert (success); + + success = FALSE; + dzl_ring_append_val (ring, data); + dzl_ring_foreach (ring, test_DzlRing_foreach_cb2, &success); + g_assert (success); + + dzl_ring_unref (ring); +} + +gint +main (gint argc, /* IN */ + gchar *argv[]) /* IN */ +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/Dazzle/Ring/index", test_index_conversion); + g_test_add_func ("/Dazzle/Ring/ref", test_DzlRing_ref); + g_test_add_func ("/Dazzle/Ring/with_double", test_DzlRing_with_double); + g_test_add_func ("/Dazzle/Ring/with_int", test_DzlRing_with_int); + g_test_add_func ("/Dazzle/Ring/with_array", test_DzlRing_with_array); + g_test_add_func ("/Dazzle/Ring/foreach", test_DzlRing_foreach); + + return g_test_run (); +} diff --git a/tests/test-shortcut-chord.c b/tests/test-shortcut-chord.c new file mode 100644 index 0000000..89d6dca --- /dev/null +++ b/tests/test-shortcut-chord.c @@ -0,0 +1,71 @@ +#include + +#define PTR1 GINT_TO_POINTER(1234) +#define PTR2 GINT_TO_POINTER(4321) + +static void +test_dzl_shortcut_chord_basic (void) +{ + DzlShortcutChord *chord; + DzlShortcutChord *chord2; + DzlShortcutChordTable *table; + DzlShortcutMatch match; + gboolean r; + gpointer data = NULL; + + chord = dzl_shortcut_chord_new_from_string ("p"); + g_assert (chord != NULL); + + table = dzl_shortcut_chord_table_new (); + dzl_shortcut_chord_table_add (table, chord, PTR1); + + match = dzl_shortcut_chord_table_lookup (table, chord, &data); + g_assert (PTR1 == data); + g_assert_cmpint (match, ==, DZL_SHORTCUT_MATCH_EQUAL); + + r = dzl_shortcut_chord_table_remove (table, chord); + g_assert_cmpint (r, ==, TRUE); + + r = dzl_shortcut_chord_table_remove (table, chord); + g_assert_cmpint (r, ==, FALSE); + + match = dzl_shortcut_chord_table_lookup (table, chord, &data); + g_assert (data == NULL); + g_assert_cmpint (match, ==, DZL_SHORTCUT_MATCH_NONE); + + chord2 = dzl_shortcut_chord_new_from_string ("p|a"); + g_assert (chord2 != NULL); + dzl_shortcut_chord_table_add (table, chord2, PTR2); + g_assert (dzl_shortcut_chord_equal (chord2, dzl_shortcut_chord_table_lookup_data (table, PTR2))); + + match = dzl_shortcut_chord_table_lookup (table, chord, &data); + g_assert (data == NULL); + g_assert_cmpint (match, ==, DZL_SHORTCUT_MATCH_PARTIAL); + + match = dzl_shortcut_chord_table_lookup (table, chord2, &data); + g_assert (data == PTR2); + g_assert_cmpint (match, ==, DZL_SHORTCUT_MATCH_EQUAL); + + g_assert (dzl_shortcut_chord_equal (chord2, dzl_shortcut_chord_table_lookup_data (table, PTR2))); + r = dzl_shortcut_chord_table_remove (table, chord2); + g_assert_cmpint (r, ==, TRUE); + g_assert (NULL == dzl_shortcut_chord_table_lookup_data (table, PTR2)); + + r = dzl_shortcut_chord_table_remove (table, chord2); + g_assert_cmpint (r, ==, FALSE); + + g_clear_pointer (&chord, dzl_shortcut_chord_free); + g_clear_pointer (&chord2, dzl_shortcut_chord_free); + g_clear_pointer (&table, dzl_shortcut_chord_table_free); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/Dazzle/Shortcut/Chord", test_dzl_shortcut_chord_basic); + + return g_test_run (); +} diff --git a/tests/test-shortcut-overlays.c b/tests/test-shortcut-overlays.c new file mode 100644 index 0000000..73e264c --- /dev/null +++ b/tests/test-shortcut-overlays.c @@ -0,0 +1,96 @@ +#include + +static void +ensure_menu_merging (DzlApplication *app) +{ + g_autofree gchar *id1 = NULL; + g_autofree gchar *id2 = NULL; + g_autofree gchar *id3 = NULL; + g_autofree gchar *label1 = NULL; + g_autoptr(GMenuModel) link1 = NULL; + g_autoptr(GMenuModel) link2 = NULL; + GMenu *menu; + guint len; + gboolean r; + + menu = dzl_application_get_menu_by_id (app, "app-menu"); + g_assert (G_IS_MENU (menu)); + + len = g_menu_model_get_n_items (G_MENU_MODEL (menu)); + g_assert_cmpint (len, ==, 4); + + g_assert (NULL != g_menu_model_get_item_link (G_MENU_MODEL (menu), 0, G_MENU_LINK_SECTION)); + + link1 = g_menu_model_get_item_link (G_MENU_MODEL (menu), 1, G_MENU_LINK_SECTION); + g_assert (link1 != NULL); + g_assert_cmpint (g_menu_model_get_n_items (link1), ==, 2); + + r = g_menu_model_get_item_attribute (link1, 0, "id", "s", &id1); + g_assert_cmpint (r, ==, TRUE); + g_assert_cmpstr (id1, ==, "section-2-item-1"); + + r = g_menu_model_get_item_attribute (link1, 0, "label", "s", &label1); + g_assert_cmpint (r, ==, TRUE); + g_assert_cmpstr (label1, ==, "Item 2.1"); + + r = g_menu_model_get_item_attribute (link1, 1, "id", "s", &id2); + g_assert_cmpint (r, ==, TRUE); + g_assert_cmpstr (id2, ==, "section-2-item-2"); + + g_assert (NULL != g_menu_model_get_item_link (G_MENU_MODEL (menu), 2, G_MENU_LINK_SECTION)); + + link2 = g_menu_model_get_item_link (G_MENU_MODEL (menu), 3, G_MENU_LINK_SECTION); + g_assert (link2 != NULL); + g_assert_cmpint (g_menu_model_get_n_items (link2), ==, 1); + + r = g_menu_model_get_item_attribute (link2, 0, "id", "s", &id3); + g_assert_cmpint (r, ==, TRUE); + g_assert_cmpstr (id3, ==, "section-4-item-1"); +} + +static void +ensure_keybinding_merging (DzlApplication *app) +{ + DzlShortcutManager *manager; + DzlShortcutTheme *theme; + + g_assert (DZL_IS_APPLICATION (app)); + + manager = dzl_application_get_shortcut_manager (app); + g_assert (DZL_IS_SHORTCUT_MANAGER (manager)); + + theme = dzl_shortcut_manager_get_theme_by_name (manager, "default"); + g_assert (DZL_IS_SHORTCUT_THEME (theme)); + + theme = dzl_shortcut_manager_get_theme_by_name (manager, "secondary"); + g_assert (DZL_IS_SHORTCUT_THEME (theme)); + g_assert_cmpstr (dzl_shortcut_theme_get_parent_name (theme), ==, "default"); +} + +static void +on_activate (DzlApplication *app) +{ + ensure_menu_merging (app); + ensure_keybinding_merging (app); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_autoptr(DzlApplication) app = NULL; + + app = g_object_new (DZL_TYPE_APPLICATION, + "application-id", "org.gnome.Dazzle.Tests.Shortcuts", + "flags", G_APPLICATION_NON_UNIQUE, + NULL); + + /* Queue resource adding, which will happen for real during startup */ + dzl_application_add_resources (app, TEST_DATA_DIR"/shortcuts/0"); + dzl_application_add_resources (app, TEST_DATA_DIR"/shortcuts/1"); + dzl_application_add_resources (app, TEST_DATA_DIR"/shortcuts/2"); + + g_signal_connect (app, "activate", G_CALLBACK (on_activate), NULL); + + return g_application_run (G_APPLICATION (app), argc, argv); +} diff --git a/tests/test-shortcut-theme.c b/tests/test-shortcut-theme.c new file mode 100644 index 0000000..20f3013 --- /dev/null +++ b/tests/test-shortcut-theme.c @@ -0,0 +1,106 @@ +#include + +#include "shortcuts/dzl-shortcut-private.h" + +static void +test_shortcut_theme_basic (void) +{ + DzlShortcutTheme *theme; + DzlShortcutContext *context; + g_autofree gchar *path = NULL; + GError *error = NULL; + gboolean r; + + theme = dzl_shortcut_theme_new (NULL); + + path = g_build_filename (TEST_DATA_DIR, "keythemes", "test.keytheme", NULL); + + if (!dzl_shortcut_theme_load_from_path (theme, path, NULL, &error)) + g_error ("%s", error->message); + + g_assert_cmpstr ("test", ==, dzl_shortcut_theme_get_name (theme)); + g_assert_cmpstr ("Test", ==, dzl_shortcut_theme_get_title (theme)); + g_assert_cmpstr ("Test theme", ==, dzl_shortcut_theme_get_subtitle (theme)); + + context = dzl_shortcut_theme_find_context_by_name (theme, "GtkWindow"); + g_assert (context != NULL); + g_assert (DZL_IS_SHORTCUT_CONTEXT (context)); + + r = dzl_shortcut_theme_save_to_path (theme, "saved.keytheme", NULL, &error); + g_assert (r); + g_assert_no_error (error); + + g_object_add_weak_pointer (G_OBJECT (context), (gpointer *)&context); + g_object_add_weak_pointer (G_OBJECT (theme), (gpointer *)&theme); + g_object_unref (theme); + g_assert (theme == NULL); + g_assert (context == NULL); +} + +static void +key_callback (GtkWidget *widget, + gpointer data) +{ + gboolean *did_cb = data; + *did_cb = TRUE; + g_assert (GTK_IS_LABEL (widget)); +} + +static void +test_shortcut_theme_manager (void) +{ + DzlShortcutController *controller; + DzlShortcutManager *manager; + DzlShortcutTheme *theme; + GtkWidget *window; + GtkWidget *label; + GdkEventKey *event; + GError *error = NULL; + gboolean did_cb = FALSE; + gboolean r; + + manager = dzl_shortcut_manager_get_default (); + g_assert (DZL_IS_SHORTCUT_MANAGER (manager)); + + theme = dzl_shortcut_manager_get_theme_by_name (manager, NULL); + g_assert (DZL_IS_SHORTCUT_THEME (theme)); + g_assert (theme == dzl_shortcut_manager_get_theme_by_name (manager, "internal")); + g_assert_cmpstr (dzl_shortcut_theme_get_parent_name (theme), ==, NULL); + + theme = dzl_shortcut_manager_get_theme_by_name (manager, "default"); + g_assert (theme == NULL); + + g_assert (G_IS_INITABLE (manager)); + r = g_initable_init (G_INITABLE (manager), NULL, &error); + g_assert_no_error (error); + g_assert_cmpint (r, ==, TRUE); + + theme = dzl_shortcut_manager_get_theme_by_name (manager, "default"); + g_assert (DZL_IS_SHORTCUT_THEME (theme)); + g_assert_cmpstr ("internal", ==, dzl_shortcut_theme_get_parent_name (theme)); + + /* Add a command and make sure we can resolve it */ + window = gtk_offscreen_window_new (); + label = gtk_label_new (NULL); + gtk_container_add (GTK_CONTAINER (window), label); + gtk_widget_show_all (window); + controller = dzl_shortcut_controller_find (label); + g_assert (DZL_IS_SHORTCUT_CONTROLLER (controller)); + dzl_shortcut_controller_add_command_callback (controller, "useless.command.here", "a", DZL_SHORTCUT_PHASE_GLOBAL, key_callback, &did_cb, NULL); + event = dzl_gdk_synthesize_event_keyval (gtk_widget_get_window (label), GDK_KEY_a); + event->state |= GDK_CONTROL_MASK; + r = dzl_shortcut_manager_handle_event (NULL, event, window); + g_assert_cmpint (did_cb, ==, TRUE); + g_assert_cmpint (r, ==, GDK_EVENT_STOP); +} + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_init (&argc, &argv); + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/ShortcutTheme/basic", test_shortcut_theme_basic); + g_test_add_func ("/Dazzle/ShortcutTheme/manager", test_shortcut_theme_manager); + return g_test_run (); +} diff --git a/tests/test-shortcuts.c b/tests/test-shortcuts.c new file mode 100644 index 0000000..9c4e387 --- /dev/null +++ b/tests/test-shortcuts.c @@ -0,0 +1,270 @@ +#include + +typedef struct +{ + GtkWindow *window; + GtkHeaderBar *header; + GtkSearchEntry *search; + DzlShortcutController *root_controller; + DzlShortcutController *search_controller; + GtkStack *stack; + GtkStackSwitcher *stack_switcher; + DzlShortcutThemeEditor *editor; + GtkButton *shortcuts; + GtkLabel *message; +} App; + +static const DzlShortcutEntry entries[] = { + { "a.b.c.a", 0, "l", "Editor", "Navigation", "Move to next error" }, + { "a.b.c.b", 0, "k", "Editor", "Navigation", "Move to previous error" }, +}; + +static void +on_shortcuts_clicked (GtkButton *button, + App *app) +{ + DzlShortcutsWindow *window; + + g_assert (GTK_IS_BUTTON (button)); + g_assert (app != NULL); + + window = g_object_new (DZL_TYPE_SHORTCUTS_WINDOW, + "transient-for", app->window, + "modal", TRUE, + NULL); + + dzl_shortcut_manager_add_shortcuts_to_window (NULL, window); + + gtk_window_present (GTK_WINDOW (window)); +} + +static void +on_current_chord_notify (DzlShortcutController *controller, + GParamSpec *pspec, + App *app) +{ + g_autofree gchar *str = NULL; + const DzlShortcutChord *chord; + + g_assert (DZL_IS_SHORTCUT_CONTROLLER (controller)); + g_assert (app != NULL); + g_assert (GTK_IS_LABEL (app->message)); + + chord = dzl_shortcut_controller_get_current_chord (controller); + str = dzl_shortcut_chord_get_label (chord); + + gtk_label_set_label (app->message, str); +} + +static void +test_callback (GtkWidget *widget, + gpointer user_data) +{ + g_assert (GTK_IS_WIDGET (widget)); + g_assert (user_data == NULL); + + g_print ("test_callback\n"); +} + +static void +a_b_c_a (GtkWidget *widget, + gpointer user_data) +{ + g_assert (GTK_IS_WIDGET (widget)); + g_assert (user_data == NULL); + + g_print ("a_b_c_a\n"); +} + +gint +main (gint argc, + gchar *argv[]) +{ + DzlShortcutManager *manager; + g_autoptr(DzlShortcutTheme) theme = NULL; + App app = { 0 }; + GtkWidget *box; + GtkWidget *separator; + +#if GTK_CHECK_VERSION(3,89,0) + gtk_init (); +#else + gtk_init (&argc, &argv); +#endif + + manager = dzl_shortcut_manager_get_default (); + + dzl_shortcut_manager_append_search_path (manager, TEST_DATA_DIR"/keythemes"); + g_initable_init (G_INITABLE (manager), NULL, NULL); + + app.window = g_object_new (GTK_TYPE_WINDOW, + "default-width", 800, + "default-height", 600, + "title", "Shortcuts Test", + NULL); + app.root_controller = dzl_shortcut_controller_new (GTK_WIDGET (app.window)); + dzl_shortcut_controller_add_command_callback (app.root_controller, "a.b.c.a", NULL, 0, a_b_c_a, NULL, NULL); + + g_signal_connect (app.root_controller, + "notify::current-chord", + G_CALLBACK (on_current_chord_notify), + &app); + + theme = g_object_ref (dzl_shortcut_manager_get_theme (manager)); + + app.header = g_object_new (GTK_TYPE_HEADER_BAR, + "show-close-button", TRUE, + "visible", TRUE, + NULL); + + app.search = g_object_new (GTK_TYPE_SEARCH_ENTRY, + "placeholder-text", "ctrl+y ctrl+y to focus", + "width-chars", 30, + "visible", TRUE, + NULL); + + gtk_window_set_titlebar (app.window, GTK_WIDGET (app.header)); + gtk_container_add (GTK_CONTAINER (app.header), GTK_WIDGET (app.search)); + + /* + * This is basically like adding to a binding set, but it allows registering + * a "command" to the signal so that users can override the command in the + * context. + * + * We also define info so that it can be displayed in the shortcuts window. + * However that work still needs to be done. + */ + app.search_controller = dzl_shortcut_controller_new (GTK_WIDGET (app.search)); + dzl_shortcut_controller_add_command_signal (app.search_controller, + "com.example.foo.search", + "y|y", + DZL_SHORTCUT_PHASE_GLOBAL, + "grab-focus", + 0); + dzl_shortcut_controller_add_command_callback (app.search_controller, + "com.example.foo.test", + "x|r", + DZL_SHORTCUT_PHASE_GLOBAL, + test_callback, NULL, NULL); + + app.shortcuts = g_object_new (GTK_TYPE_BUTTON, + "label", "Shortcuts", + "visible", TRUE, + NULL); + g_signal_connect (app.shortcuts, + "clicked", + G_CALLBACK (on_shortcuts_clicked), + &app); + gtk_container_add (GTK_CONTAINER (app.header), GTK_WIDGET (app.shortcuts)); + + box = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_VERTICAL, + "visible", TRUE, + NULL); + app.stack = g_object_new (GTK_TYPE_STACK, + "expand", TRUE, + "visible", TRUE, + NULL); + app.stack_switcher = g_object_new (GTK_TYPE_STACK_SWITCHER, + "margin-top", 6, + "margin-bottom", 6, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "halign", GTK_ALIGN_CENTER, + "hexpand", TRUE, + "stack", app.stack, + "visible", FALSE, + NULL); + + app.editor = g_object_new (DZL_TYPE_SHORTCUT_THEME_EDITOR, + "theme", dzl_shortcut_manager_get_theme (NULL), + "width-request", 600, + "halign", GTK_ALIGN_CENTER, + "margin", 32, + "vexpand", TRUE, + "visible", TRUE, + NULL); + gtk_container_add_with_properties (GTK_CONTAINER (app.stack), GTK_WIDGET (app.editor), + "name", "editor", + "title", "Shortcuts Editor", + "position", 0, + NULL); + + separator = g_object_new (GTK_TYPE_SEPARATOR, + "visible", TRUE, + NULL); + + app.message = g_object_new (GTK_TYPE_LABEL, + "label", "Ready.", + "margin", 6, + "xalign", 0.0f, + "visible", TRUE, + NULL); + + gtk_container_add (GTK_CONTAINER (app.window), GTK_WIDGET (box)); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (app.stack_switcher)); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (app.stack)); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (separator)); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (app.message)); + + dzl_shortcut_manager_add_action (manager, "build-manager.build", "Projects", "Building", "Build Project", NULL); + dzl_shortcut_manager_add_action (manager, "build-manager.rebuild", "Projects", "Building", "Rebuild Project", NULL); + dzl_shortcut_manager_add_action (manager, "build-manager.clean", "Projects", "Building", "Clean Project", NULL); + + dzl_shortcut_manager_add_action (manager, "run-manager.run", "Projects", "Running", "Run Project", NULL); + dzl_shortcut_manager_add_action (manager, "run-manager.run-with-handler::'profiler'", "Projects", "Running", "Run Profiler", NULL); + dzl_shortcut_manager_add_action (manager, "run-manager.run-with-handler::'debugger'", "Projects", "Running", "Run Debugger", NULL); + + dzl_shortcut_manager_add_action (manager, "win.new-terminal", "Terminal", "Terminal", "New terminal on host", NULL); + dzl_shortcut_manager_add_action (manager, "win.new-terminal-in-runtime", "Terminal", "Terminal", "New terminal in build runtime", NULL); + + dzl_shortcut_manager_add_command (manager, "org.gnome.builder.cut", "Editor", "Editing", "Cut Selection", NULL); + dzl_shortcut_manager_add_command (manager, "org.gnome.builder.copy", "Editor", "Editing", "Copy Selection", NULL); + dzl_shortcut_manager_add_command (manager, "org.gnome.builder.paste", "Editor", "Editing", "Paste Selection", NULL); + dzl_shortcut_manager_add_command (manager, "org.gnome.builder.delete", "Editor", "Editing", "Delete Selection", NULL); + + dzl_shortcut_theme_set_accel_for_action (theme, "build-manager.build", "F7", 0); + dzl_shortcut_theme_set_accel_for_action (theme, "build-manager.rebuild", "F7", 0); + dzl_shortcut_theme_set_accel_for_action (theme, "build-manager.clean", "F8", 0); + + dzl_shortcut_theme_set_accel_for_action (theme, "win.new-terminal", "t", 0); + dzl_shortcut_theme_set_accel_for_action (theme, "win.new-terminal-in-runtime", "t", 0); + + dzl_shortcut_theme_set_accel_for_command (theme, "org.gnome.builder.cut", "x", 0); + dzl_shortcut_theme_set_accel_for_command (theme, "org.gnome.builder.copy", "c", 0); + dzl_shortcut_theme_set_accel_for_command (theme, "org.gnome.builder.paste", "v", 0); + dzl_shortcut_theme_set_accel_for_command (theme, "org.gnome.builder.delete", "Delete", 0); + + dzl_shortcut_theme_set_accel_for_command (theme, "com.example.foo.test", "r|r", 0); + + dzl_shortcut_theme_set_accel_for_action (theme, "run-manager.run", "F3", 0); + dzl_shortcut_theme_set_accel_for_action (theme, "run-manager.run-with-handler::'debugger'", "F3", 0); + + dzl_shortcut_manager_add_shortcut_entries (manager, entries, G_N_ELEMENTS (entries), NULL); + + dzl_shortcut_manager_set_theme_name (manager, "test"); + + g_signal_emit_by_name (manager, "changed"); + + g_signal_connect_swapped (app.window, + "key-press-event", + G_CALLBACK (dzl_shortcut_manager_handle_event), + manager); + g_signal_connect (app.window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (app.window); + + { + guint len = g_list_model_get_n_items (G_LIST_MODEL (manager)); + + for (guint i = 0; i < len; i++) + { + g_autoptr(DzlShortcutTheme) t = g_list_model_get_item (G_LIST_MODEL (manager), i); + const gchar *name = dzl_shortcut_theme_get_name (t); + + g_print ("Theme = %s\n", name); + } + } + + gtk_main (); + + return 0; +} diff --git a/tests/test-signal-group.c b/tests/test-signal-group.c new file mode 100644 index 0000000..6e6067d --- /dev/null +++ b/tests/test-signal-group.c @@ -0,0 +1,488 @@ + +#define G_LOG_DOMAIN "dzl-signal-group" + +#include + +typedef struct _SignalTarget +{ + GObject parent_instance; +} SignalTarget; + +G_DECLARE_FINAL_TYPE (SignalTarget, signal_target, TEST, SIGNAL_TARGET, GObject) +G_DEFINE_TYPE (SignalTarget, signal_target, G_TYPE_OBJECT) + +static +G_DEFINE_QUARK (detail, signal_detail) + +enum { + THE_SIGNAL, + NEVER_EMITTED, + LAST_SIGNAL +}; + +static guint signals [LAST_SIGNAL]; + +static void +signal_target_class_init (SignalTargetClass *klass) +{ + signals [THE_SIGNAL] = + g_signal_new ("the-signal", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_OBJECT); + + signals [NEVER_EMITTED] = + g_signal_new ("never-emitted", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_OBJECT); +} + +static void +signal_target_init (SignalTarget *self) +{ +} + +static gint global_signal_calls; +static gint global_weak_notify_called; + +static void +connect_before_cb (SignalTarget *target, + DzlSignalGroup *group, + gint *signal_calls) +{ + g_assert (TEST_IS_SIGNAL_TARGET (target)); + g_assert (DZL_IS_SIGNAL_GROUP (group)); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target); + g_assert (signal_calls != NULL); + g_assert (signal_calls == &global_signal_calls); + + *signal_calls += 1; +} + +static void +connect_after_cb (SignalTarget *target, + DzlSignalGroup *group, + gint *signal_calls) +{ + g_assert (TEST_IS_SIGNAL_TARGET (target)); + g_assert (DZL_IS_SIGNAL_GROUP (group)); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target); + g_assert (signal_calls != NULL); + g_assert (signal_calls == &global_signal_calls); + + g_assert_cmpint (*signal_calls, ==, 4); + *signal_calls += 1; +} + +static void +connect_swapped_cb (gint *signal_calls, + DzlSignalGroup *group, + SignalTarget *target) +{ + g_assert (signal_calls != NULL); + g_assert (signal_calls == &global_signal_calls); + g_assert (DZL_IS_SIGNAL_GROUP (group)); + g_assert (TEST_IS_SIGNAL_TARGET (target)); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target); + + *signal_calls += 1; +} + +static void +connect_object_cb (SignalTarget *target, + DzlSignalGroup *group, + GObject *object) +{ + gint *signal_calls; + + g_assert (TEST_IS_SIGNAL_TARGET (target)); + g_assert (DZL_IS_SIGNAL_GROUP (group)); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target); + g_assert (G_IS_OBJECT (object)); + + signal_calls = g_object_get_data (object, "signal-calls"); + g_assert (signal_calls != NULL); + g_assert (signal_calls == &global_signal_calls); + + *signal_calls += 1; +} + +static void +connect_bad_detail_cb (SignalTarget *target, + DzlSignalGroup *group, + GObject *object) +{ + g_error ("This detailed signal is never emitted!"); +} + +static void +connect_never_emitted_cb (SignalTarget *target, + gboolean *weak_notify_called) +{ + g_error ("This signal is never emitted!"); +} + +static void +connect_data_notify_cb (gboolean *weak_notify_called, + GClosure *closure) +{ + g_assert (weak_notify_called != NULL); + g_assert (weak_notify_called == &global_weak_notify_called); + g_assert (closure != NULL); + + g_assert_false (*weak_notify_called); + *weak_notify_called = TRUE; +} + +static void +connect_data_weak_notify_cb (gboolean *weak_notify_called, + DzlSignalGroup *group) +{ + g_assert (weak_notify_called != NULL); + g_assert (weak_notify_called == &global_weak_notify_called); + g_assert (DZL_IS_SIGNAL_GROUP (group)); + + g_assert_true (*weak_notify_called); +} + +static void +connect_all_signals (DzlSignalGroup *group) +{ + GObject *object; + + /* Check that these are called in the right order */ + dzl_signal_group_connect (group, + "the-signal", + G_CALLBACK (connect_before_cb), + &global_signal_calls); + dzl_signal_group_connect_after (group, + "the-signal", + G_CALLBACK (connect_after_cb), + &global_signal_calls); + + /* Check that this is called with the arguments swapped */ + dzl_signal_group_connect_swapped (group, + "the-signal", + G_CALLBACK (connect_swapped_cb), + &global_signal_calls); + + /* Check that this is called with the arguments swapped */ + object = g_object_new (G_TYPE_OBJECT, NULL); + g_object_set_data (object, "signal-calls", &global_signal_calls); + dzl_signal_group_connect_object (group, + "the-signal", + G_CALLBACK (connect_object_cb), + object, + 0); + g_object_weak_ref (G_OBJECT (group), + (GWeakNotify)g_object_unref, + object); + + /* Check that a detailed signal is handled correctly */ + dzl_signal_group_connect (group, + "the-signal::detail", + G_CALLBACK (connect_before_cb), + &global_signal_calls); + dzl_signal_group_connect (group, + "the-signal::bad-detail", + G_CALLBACK (connect_bad_detail_cb), + NULL); + + /* Check that the notify is called correctly */ + global_weak_notify_called = FALSE; + dzl_signal_group_connect_data (group, + "never-emitted", + G_CALLBACK (connect_never_emitted_cb), + &global_weak_notify_called, + (GClosureNotify)connect_data_notify_cb, + 0); + g_object_weak_ref (G_OBJECT (group), + (GWeakNotify)connect_data_weak_notify_cb, + &global_weak_notify_called); +} + +static void +assert_signals (SignalTarget *target, + DzlSignalGroup *group, + gboolean success) +{ + g_assert (TEST_IS_SIGNAL_TARGET (target)); + g_assert (group == NULL || DZL_IS_SIGNAL_GROUP (group)); + + global_signal_calls = 0; + g_signal_emit (target, signals [THE_SIGNAL], + signal_detail_quark (), group); + g_assert_cmpint (global_signal_calls, ==, success ? 5 : 0); +} + +static void +test_signal_group_invalid (void) +{ + GObject *invalid_target = g_object_new (G_TYPE_OBJECT, NULL); + SignalTarget *target = g_object_new (signal_target_get_type (), NULL); + DzlSignalGroup *group = dzl_signal_group_new (signal_target_get_type ()); + + /* Invalid Target Type */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*g_type_is_a*G_TYPE_OBJECT*"); + dzl_signal_group_new (G_TYPE_DATE_TIME); + g_test_assert_expected_messages (); + + /* Invalid Target */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*Failed to set DzlSignalGroup of target " + "type SignalTarget using target * of type GObject*"); + dzl_signal_group_set_target (group, invalid_target); + g_test_assert_expected_messages (); + + /* Invalid Signal Name */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*g_signal_parse_name*"); + dzl_signal_group_connect (group, + "does-not-exist", + G_CALLBACK (connect_before_cb), + NULL); + g_test_assert_expected_messages (); + + /* Invalid Callback */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*callback != NULL*"); + dzl_signal_group_connect (group, + "the-signal", + G_CALLBACK (NULL), + NULL); + g_test_assert_expected_messages (); + + g_object_unref (group); + g_object_unref (target); + g_object_unref (invalid_target); +} + +static void +test_signal_group_simple (void) +{ + SignalTarget *target = g_object_new (signal_target_get_type (), NULL); + DzlSignalGroup *group = dzl_signal_group_new (signal_target_get_type ()); + + /* Set the target before connecting the signals */ + g_assert_null (dzl_signal_group_get_target (group)); + dzl_signal_group_set_target (group, target); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target); + + connect_all_signals (group); + assert_signals (target, group, TRUE); + + /* Destroying the SignalGroup should disconnect the signals */ + g_object_unref (group); + assert_signals (target, NULL, FALSE); + + g_object_unref (target); +} + +static void +test_signal_group_changing_target (void) +{ + SignalTarget *target1, *target2; + DzlSignalGroup *group = dzl_signal_group_new (signal_target_get_type ()); + + connect_all_signals (group); + g_assert_null (dzl_signal_group_get_target (group)); + + /* Set the target after connecting the signals */ + target1 = g_object_new (signal_target_get_type (), NULL); + dzl_signal_group_set_target (group, target1); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target1); + + assert_signals (target1, group, TRUE); + + /* Set the same target */ + g_assert (dzl_signal_group_get_target (group) == (GObject *)target1); + dzl_signal_group_set_target (group, target1); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target1); + + assert_signals (target1, group, TRUE); + + /* Set a new target when the current target is non-NULL */ + target2 = g_object_new (signal_target_get_type (), NULL); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target1); + dzl_signal_group_set_target (group, target2); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target2); + + assert_signals (target2, group, TRUE); + + g_object_unref (target2); + g_object_unref (target1); + g_object_unref (group); +} + +static void +assert_blocking (SignalTarget *target, + DzlSignalGroup *group, + gint count) +{ + gint i; + + assert_signals (target, group, TRUE); + + /* Assert that multiple blocks are effective */ + for (i = 0; i < count; ++i) + { + dzl_signal_group_block (group); + assert_signals (target, group, FALSE); + } + + /* Assert that the signal is not emitted after the first unblock */ + for (i = 0; i < count; ++i) + { + assert_signals (target, group, FALSE); + dzl_signal_group_unblock (group); + } + + assert_signals (target, group, TRUE); +} + +static void +test_signal_group_blocking (void) +{ + SignalTarget *target1, *target2; + DzlSignalGroup *group = dzl_signal_group_new (signal_target_get_type ()); + + connect_all_signals (group); + g_assert_null (dzl_signal_group_get_target (group)); + + target1 = g_object_new (signal_target_get_type (), NULL); + dzl_signal_group_set_target (group, target1); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target1); + + assert_blocking (target1, group, 1); + assert_blocking (target1, group, 3); + assert_blocking (target1, group, 15); + + /* Assert that blocking transfers across changing the target */ + dzl_signal_group_block (group); + dzl_signal_group_block (group); + + /* Set a new target when the current target is non-NULL */ + target2 = g_object_new (signal_target_get_type (), NULL); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target1); + dzl_signal_group_set_target (group, target2); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target2); + + assert_signals (target2, group, FALSE); + dzl_signal_group_unblock (group); + assert_signals (target2, group, FALSE); + dzl_signal_group_unblock (group); + assert_signals (target2, group, TRUE); + + g_object_unref (target2); + g_object_unref (target1); + g_object_unref (group); +} + +static void +test_signal_group_weak_ref_target (void) +{ + SignalTarget *target = g_object_new (signal_target_get_type (), NULL); + DzlSignalGroup *group = dzl_signal_group_new (signal_target_get_type ()); + + g_assert_null (dzl_signal_group_get_target (group)); + dzl_signal_group_set_target (group, target); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target); + + g_object_add_weak_pointer (G_OBJECT (target), (gpointer)&target); + g_object_unref (target); + g_assert_null (target); + g_assert_null (dzl_signal_group_get_target (group)); + + g_object_unref (group); +} + +static void +test_signal_group_connect_object (void) +{ + GObject *object = g_object_new (G_TYPE_OBJECT, NULL); + SignalTarget *target = g_object_new (signal_target_get_type (), NULL); + DzlSignalGroup *group = dzl_signal_group_new (signal_target_get_type ()); + + /* We already do basic connect_object() tests in connect_signals(), + * this is only needed to test the specifics of connect_object() + */ + dzl_signal_group_connect_object (group, + "the-signal", + G_CALLBACK (connect_object_cb), + object, + 0); + + g_assert_null (dzl_signal_group_get_target (group)); + dzl_signal_group_set_target (group, target); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target); + + g_object_add_weak_pointer (G_OBJECT (object), (gpointer)&object); + g_object_unref (object); + g_assert_null (object); + + /* This would cause a warning if the SignalGroup did not + * have a weakref on the object as it would try to connect again + */ + dzl_signal_group_set_target (group, NULL); + g_assert (dzl_signal_group_get_target (group) == (GObject *)NULL); + dzl_signal_group_set_target (group, target); + g_assert (dzl_signal_group_get_target (group) == (GObject *)target); + + g_object_unref (group); + g_object_unref (target); +} + +static void +test_signal_group_signal_parsing (void) +{ + g_test_trap_subprocess ("/Dazzle/SignalGroup/signal-parsing/subprocess", 0, + G_TEST_SUBPROCESS_INHERIT_STDERR); + g_test_trap_assert_passed (); + g_test_trap_assert_stderr (""); +} + +static void +test_signal_group_signal_parsing_subprocess (void) +{ + DzlSignalGroup *group; + + /* Check that the class has not been created and with it the + * signals registered. This will cause g_signal_parse_name() + * to fail unless DzlSignalGroup calls g_type_class_ref(). + */ + g_assert_null (g_type_class_peek (signal_target_get_type ())); + + group = dzl_signal_group_new (signal_target_get_type ()); + dzl_signal_group_connect (group, + "the-signal", + G_CALLBACK (connect_before_cb), + NULL); + + g_object_unref (group); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/SignalGroup/invalid", test_signal_group_invalid); + g_test_add_func ("/Dazzle/SignalGroup/simple", test_signal_group_simple); + g_test_add_func ("/Dazzle/SignalGroup/changing-target", test_signal_group_changing_target); + g_test_add_func ("/Dazzle/SignalGroup/blocking", test_signal_group_blocking); + g_test_add_func ("/Dazzle/SignalGroup/weak-ref-target", test_signal_group_weak_ref_target); + g_test_add_func ("/Dazzle/SignalGroup/connect-object", test_signal_group_connect_object); + g_test_add_func ("/Dazzle/SignalGroup/signal-parsing", test_signal_group_signal_parsing); + g_test_add_func ("/Dazzle/SignalGroup/signal-parsing/subprocess", test_signal_group_signal_parsing_subprocess); + return g_test_run (); +} diff --git a/tests/test-simple-popover.c b/tests/test-simple-popover.c new file mode 100644 index 0000000..76702ad --- /dev/null +++ b/tests/test-simple-popover.c @@ -0,0 +1,60 @@ +#include + +static void +text_changed (DzlSimplePopover *popover, + gpointer unused) +{ + const gchar *text; + + text = dzl_simple_popover_get_text (popover); + dzl_simple_popover_set_ready (popover, text && *text); +} + +static void +activate_cb (DzlSimplePopover *popover, + const gchar *text, + GtkButton *button) +{ + g_assert (DZL_IS_SIMPLE_POPOVER (popover)); + g_assert (GTK_IS_BUTTON (button)); + + gtk_button_set_label (button, text); +} + +gint +main (gint argc, + gchar *argv[]) +{ + DzlSimplePopover *popover; + GtkMenuButton *button; + GtkWindow *window; + + gtk_init (&argc, &argv); + + window = g_object_new (GTK_TYPE_WINDOW, + "title", "Test Simple Popover", + "border-width", 24, + "visible", TRUE, + NULL); + + popover = g_object_new (DZL_TYPE_SIMPLE_POPOVER, + "title", "Change Label", + "message", "Type the new text for the label", + "button-text", "Change", + NULL); + + button = g_object_new (GTK_TYPE_MENU_BUTTON, + "label", "Click Me…", + "popover", popover, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (button)); + + g_signal_connect (popover, "changed", G_CALLBACK (text_changed), NULL); + g_signal_connect (popover, "activate", G_CALLBACK (activate_cb), button); + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + + gtk_main (); + + return 0; +} diff --git a/tests/test-slider.c b/tests/test-slider.c new file mode 100644 index 0000000..35469ed --- /dev/null +++ b/tests/test-slider.c @@ -0,0 +1,58 @@ + +#include +#include + +static void +connect_button (GtkBuilder *builder, + const gchar *name, + GCallback callback) +{ + DzlSlider *slider = DZL_SLIDER (gtk_builder_get_object (builder, "slider")); + GtkButton *button = GTK_BUTTON (gtk_builder_get_object (builder, name)); + + g_assert (slider != NULL); + g_assert (button != NULL); + + g_signal_connect_swapped (button, "clicked", callback, slider); +} + +static void set_bottom (DzlSlider *slider) { dzl_slider_set_position (slider, DZL_SLIDER_BOTTOM); } +static void set_top (DzlSlider *slider) { dzl_slider_set_position (slider, DZL_SLIDER_TOP); } +static void set_left (DzlSlider *slider) { dzl_slider_set_position (slider, DZL_SLIDER_LEFT); } +static void set_right (DzlSlider *slider) { dzl_slider_set_position (slider, DZL_SLIDER_RIGHT); } +static void set_none (DzlSlider *slider) { dzl_slider_set_position (slider, DZL_SLIDER_NONE); } + +int +main (int argc, + char *argv[]) +{ + GtkBuilder *builder; + GObject *window; + GError *error = NULL; + g_autofree gchar *path = g_build_filename (TEST_DATA_DIR, "test-slider.ui", NULL); + + gtk_init (&argc, &argv); + + builder = gtk_builder_new (); + gtk_builder_add_from_file (builder, path, &error); + g_assert_no_error (error); + + window = gtk_builder_get_object (builder, "window"); + g_assert (window != NULL); + + connect_button (builder, "up_button", G_CALLBACK (set_bottom)); + connect_button (builder, "down_button", G_CALLBACK (set_top)); + connect_button (builder, "end_button", G_CALLBACK (set_left)); + connect_button (builder, "start_button", G_CALLBACK (set_right)); + connect_button (builder, "none_button", G_CALLBACK (set_none)); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + + gtk_window_present (GTK_WINDOW (window)); + + gtk_main (); + + g_object_unref (builder); + + return EXIT_SUCCESS; +} diff --git a/tests/test-stack-list.c b/tests/test-stack-list.c new file mode 100644 index 0000000..01efed8 --- /dev/null +++ b/tests/test-stack-list.c @@ -0,0 +1,149 @@ +#include + +static GtkWidget * +create_child_func (gpointer item, + gpointer user_data) +{ + GFileInfo *file_info = G_FILE_INFO (item); + g_autofree gchar *display_name = NULL; + GObject *icon = NULL; + GtkListBoxRow *row; + GtkWidget *label; + GFile *parent = user_data; + GtkBox *box; + GtkImage *image; + + g_assert (!file_info || G_IS_FILE_INFO (file_info)); + g_assert (!parent || G_IS_FILE (parent)); + + if (file_info == NULL) + { + if (parent == NULL) + display_name = g_strdup ("Computer"); + else + display_name = g_file_get_basename (parent); + } + else + { + display_name = g_strdup (g_file_info_get_display_name (file_info)); + icon = g_file_info_get_attribute_object (file_info, G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON); + } + + + box = g_object_new (GTK_TYPE_BOX, + "visible", TRUE, + NULL); + + image = g_object_new (GTK_TYPE_IMAGE, + "visible", TRUE, + "gicon", icon, + NULL); + gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (image)); + + label = g_object_new (GTK_TYPE_LABEL, + "label", display_name, + "halign", GTK_ALIGN_START, + "hexpand", TRUE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (box), label); + + row = g_object_new (GTK_TYPE_LIST_BOX_ROW, + "visible", TRUE, + "child", box, + NULL); + + if (parent != NULL) + g_object_set_data_full (G_OBJECT (row), "FILE", + g_file_get_child (parent, g_file_info_get_name (file_info)), + g_object_unref); + + return GTK_WIDGET (row); +} + +static void +row_activated (DzlStackList *stack_list, + GtkListBoxRow *row, + gpointer user_data) +{ + GFile *file = G_FILE (g_object_get_data (G_OBJECT (row), "FILE")); + GFile *parent = g_file_get_parent (file); + g_autoptr(GListModel) model = dzl_directory_model_new (file); + g_autofree gchar *name = g_file_get_basename (file); + g_autoptr(GFileInfo) file_info = g_file_info_new (); + + g_file_info_set_name (file_info, name); + g_file_info_set_display_name (file_info, name); + + dzl_stack_list_push (stack_list, + create_child_func (file_info, parent), + model, + create_child_func, + g_object_ref (file), + g_object_unref); +} + +static void +load_css (void) +{ + g_autoptr(GtkCssProvider) provider = NULL; + + provider = dzl_css_provider_new ("resource:///org/gnome/dazzle/themes"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_autoptr(GListModel) file_system_model = NULL; + g_autoptr(GFile) root = NULL; + DzlStackList *stack_list; + GtkWidget *window; + GtkWidget *header; + + gtk_init (&argc, &argv); + + load_css (); + + root = g_file_new_for_path ("/"); + file_system_model = dzl_directory_model_new (root); + + window = g_object_new (GTK_TYPE_WINDOW, + "default-width", 250, + "default-height", 600, + NULL); + + header = g_object_new (GTK_TYPE_HEADER_BAR, + "title", "Stack List Test", + "show-close-button", TRUE, + "visible", TRUE, + NULL); + gtk_window_set_titlebar (GTK_WINDOW (window), header); + + stack_list = g_object_new (DZL_TYPE_STACK_LIST, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (stack_list)); + + dzl_stack_list_push (stack_list, + create_child_func (NULL, root), + file_system_model, + create_child_func, + g_steal_pointer (&root), + g_object_unref); + + g_signal_connect (stack_list, + "row-activated", + G_CALLBACK (row_activated), + NULL); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + + gtk_main (); + + return 0; +} diff --git a/tests/test-state-machine.c b/tests/test-state-machine.c new file mode 100644 index 0000000..bc422b9 --- /dev/null +++ b/tests/test-state-machine.c @@ -0,0 +1,371 @@ +/* test-dzl-state-machine.c + * + * Copyright (C) 2015 Christian Hergert + * + * 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 . + */ + +#include + +struct _TestObject +{ + GObject parent_instance; + + gint obj1_count; + gint obj2_count; + + gchar *str; +}; + +#define TEST_TYPE_OBJECT (test_object_get_type()) +G_DECLARE_FINAL_TYPE (TestObject, test_object, TEST, OBJECT, GObject) +G_DEFINE_TYPE (TestObject, test_object, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_STRING, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +static void +get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + TestObject *obj = (TestObject *)object; + + switch (prop_id) + { + case PROP_STRING: + g_value_set_string (value, obj->str); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + TestObject *obj = (TestObject *)object; + + switch (prop_id) + { + case PROP_STRING: + g_free (obj->str); + obj->str = g_value_dup_string (value); + g_object_notify_by_pspec (object, pspec); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +finalize (GObject *object) +{ + TestObject *self = (TestObject *)object; + + g_free (self->str); + + G_OBJECT_CLASS (test_object_parent_class)->finalize (object); +} + +static void +test_object_class_init (TestObjectClass *klass) +{ + GObjectClass *obj_class = G_OBJECT_CLASS (klass); + + obj_class->finalize = finalize; + obj_class->get_property = get_property; + obj_class->set_property = set_property; + + properties [PROP_STRING] = + g_param_spec_string ("string", + "string", + "string", + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (obj_class, LAST_PROP, properties); + + g_signal_new ("frobnicate", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); +} + +static void +test_object_init (TestObject *self) +{ +} + +static void +obj1_frobnicate (TestObject *dummy, + TestObject *source) +{ + g_assert (TEST_IS_OBJECT (dummy)); + g_assert (TEST_IS_OBJECT (source)); + + dummy->obj1_count++; +} + +static void +obj2_frobnicate (TestObject *dummy, + TestObject *source) +{ + g_assert (TEST_IS_OBJECT (dummy)); + g_assert (TEST_IS_OBJECT (source)); + + dummy->obj2_count++; +} + +static void +assert_prop_equal (gpointer obja, + gpointer objb, + const gchar *propname) +{ + GParamSpec *pspec; + GValue va = G_VALUE_INIT; + GValue vb = G_VALUE_INIT; + + pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (obja), propname); + g_assert (pspec != NULL); + + g_value_init (&va, pspec->value_type); + g_value_init (&vb, pspec->value_type); + + g_object_get_property (obja, propname, &va); + g_object_get_property (objb, propname, &vb); + +#define ADD_CHECK(NAME, name, cmp_type) \ + case G_TYPE_##NAME: \ + g_assert_cmp##cmp_type (g_value_get_##name (&va), ==, g_value_get_##name (&vb)); \ + break + + switch (pspec->value_type) + { + ADD_CHECK (INT, int, int); + ADD_CHECK (BOOLEAN, boolean, int); + + ADD_CHECK (UINT, uint, uint); + + ADD_CHECK (FLOAT, float, float); + ADD_CHECK (DOUBLE, double, float); + + ADD_CHECK (STRING, string, str); + + default: + g_assert_not_reached (); + } + + g_value_unset (&va); + g_value_unset (&vb); +} + +#if 0 +static gboolean +has_style_class (GtkWidget *widget, + const gchar *class_name) +{ + GtkStyleContext *style_context; + + style_context = gtk_widget_get_style_context (widget); + return gtk_style_context_has_class (style_context, class_name); +} +#endif + +static void +test_state_machine_basic (void) +{ + DzlStateMachine *machine; + GSimpleAction *action; + TestObject *dummy; + TestObject *obj1; + TestObject *obj2; + + machine = dzl_state_machine_new (); + g_object_add_weak_pointer (G_OBJECT (machine), (gpointer *)&machine); + + action = g_simple_action_new ("my-action", NULL); + dummy = g_object_new (TEST_TYPE_OBJECT, NULL); + obj1 = g_object_new (TEST_TYPE_OBJECT, NULL); + obj2 = g_object_new (TEST_TYPE_OBJECT, NULL); + + g_simple_action_set_enabled (action, FALSE); + +#if 0 + g_print ("obj1=%p obj2=%p dummy=%p\n", obj1, obj2, dummy); +#endif + + dzl_state_machine_connect_object (machine, "state1", obj1, "frobnicate", + G_CALLBACK (obj1_frobnicate), dummy, G_CONNECT_SWAPPED); + dzl_state_machine_connect_object (machine, "state2", obj2, "frobnicate", + G_CALLBACK (obj2_frobnicate), dummy, G_CONNECT_SWAPPED); + + dzl_state_machine_add_binding (machine, "state1", obj1, "string", dummy, "string", 0); + dzl_state_machine_add_binding (machine, "state2", obj2, "string", dummy, "string", 0); + + dzl_state_machine_add_property (machine, "state1", action, "enabled", TRUE); + dzl_state_machine_add_property (machine, "state2", action, "enabled", FALSE); + dzl_state_machine_add_property (machine, "state3", action, "enabled", FALSE); + + g_assert_false (g_action_get_enabled (G_ACTION (action))); + + dzl_state_machine_set_state (machine, "state1"); + g_assert_cmpstr (dzl_state_machine_get_state (machine), ==, "state1"); + g_assert_cmpint (dummy->obj1_count, ==, 0); + g_assert_cmpint (dummy->obj2_count, ==, 0); + + g_assert_true (g_action_get_enabled (G_ACTION (action))); + + g_signal_emit_by_name (obj1, "frobnicate"); + g_assert_cmpint (dummy->obj1_count, ==, 1); + g_assert_cmpint (dummy->obj2_count, ==, 0); + + g_signal_emit_by_name (obj2, "frobnicate"); + g_assert_cmpint (dummy->obj1_count, ==, 1); + g_assert_cmpint (dummy->obj2_count, ==, 0); + + dzl_state_machine_set_state (machine, "state2"); + g_assert_cmpstr (dzl_state_machine_get_state (machine), ==, "state2"); + + g_assert_false (g_action_get_enabled (G_ACTION (action))); + + g_signal_emit_by_name (obj1, "frobnicate"); + g_assert_cmpint (dummy->obj1_count, ==, 1); + g_assert_cmpint (dummy->obj2_count, ==, 0); + + g_signal_emit_by_name (obj2, "frobnicate"); + g_assert_cmpint (dummy->obj1_count, ==, 1); + g_assert_cmpint (dummy->obj2_count, ==, 1); + + g_object_set (obj2, "string", "obj2", NULL); + g_object_set (obj1, "string", "obj1", NULL); + assert_prop_equal (obj2, dummy, "string"); + + dzl_state_machine_set_state (machine, "state3"); + dzl_state_machine_set_state (machine, "state1"); + + assert_prop_equal (obj1, dummy, "string"); + g_object_set (obj1, "string", "obj1-1", NULL); + assert_prop_equal (obj1, dummy, "string"); + g_object_set (obj2, "string", "obj2-1", NULL); + assert_prop_equal (obj1, dummy, "string"); + + dzl_state_machine_set_state (machine, "state3"); + + g_object_unref (machine); + g_assert (machine == NULL); + + g_clear_object (&action); +} + +#define assert_final_ref(o) \ + G_STMT_START \ + { \ + GObject **object_ptr = (GObject **)o; \ +\ + g_object_add_weak_pointer (*object_ptr, (gpointer *)object_ptr); \ + g_object_unref (*object_ptr); \ + g_assert_null (*object_ptr); \ + } \ + G_STMT_END + + +/* This test exposed multiple bugs in GObject: + * https://bugzilla.gnome.org/show_bug.cgi?id=749659 + * https://bugzilla.gnome.org/show_bug.cgi?id=749660 + */ +#if 0 +static void +test_state_machine_weak_ref_source (void) +{ + DzlStateMachine *machine; + GSimpleAction *action; + TestObject *dummy; + TestObject *obj; + GtkWidget *widget; + + machine = dzl_state_machine_new (); + + action = g_simple_action_new ("my-action", NULL); + dummy = g_object_new (TEST_TYPE_OBJECT, NULL); + obj = g_object_new (TEST_TYPE_OBJECT, NULL); + widget = g_object_ref_sink (gtk_event_box_new ()); + + g_simple_action_set_enabled (action, FALSE); + + dzl_state_machine_connect_object (machine, "state", obj, "frobnicate", + G_CALLBACK (obj1_frobnicate), dummy, G_CONNECT_SWAPPED); + dzl_state_machine_add_binding (machine, "state", obj, "string", dummy, "string", 0); + dzl_state_machine_add_property (machine, "state", action, "enabled", TRUE); + dzl_state_machine_add_style (machine, "state", widget, "testing"); + + dzl_state_machine_set_state (machine, "state"); + g_assert_cmpstr (dzl_state_machine_get_state (machine), ==, "state"); + + /* Check that everything is working */ + g_signal_emit_by_name (obj, "frobnicate"); + g_assert_cmpint (dummy->obj1_count, ==, 1); + g_object_set (obj, "string", "hello world", NULL); + assert_prop_equal (obj, dummy, "string"); + g_assert_true (g_action_get_enabled (G_ACTION (action))); + g_assert (has_style_class (widget, "testing")); + + /* Destroy the source objects while still in the state */ + assert_final_ref (&widget); + assert_final_ref (&obj); + assert_final_ref (&dummy); + assert_final_ref (&action); + + /* Go back and forth between the states as this would cause + * a warning if the source objects did not have a weakref on them + */ + dzl_state_machine_set_state (machine, NULL); + g_assert_cmpstr (dzl_state_machine_get_state (machine), ==, NULL); + dzl_state_machine_set_state (machine, "state"); + g_assert_cmpstr (dzl_state_machine_get_state (machine), ==, "state"); + dzl_state_machine_set_state (machine, "empty"); + g_assert_cmpstr (dzl_state_machine_get_state (machine), ==, "empty"); + dzl_state_machine_set_state (machine, "state"); + g_assert_cmpstr (dzl_state_machine_get_state (machine), ==, "state"); + dzl_state_machine_set_state (machine, NULL); + g_assert_cmpstr (dzl_state_machine_get_state (machine), ==, NULL); + + assert_final_ref (&machine); +} +#endif + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + gtk_init (&argc, &argv); + g_test_add_func ("/Dazzle/StateMachine/basic", test_state_machine_basic); + /*g_test_add_func ("/Dzl/StateMachine/weak-ref-source", test_state_machine_weak_ref_source);*/ + return g_test_run (); +} diff --git a/tests/test-suggestion-buffer.c b/tests/test-suggestion-buffer.c new file mode 100644 index 0000000..4d63377 --- /dev/null +++ b/tests/test-suggestion-buffer.c @@ -0,0 +1,102 @@ +#include + +static gchar * +suggest_suffix (DzlSuggestion *suggestion, + const gchar *query, + const gchar *suffix) +{ + return g_strdup (suffix); +} + +static void +test_basic (void) +{ + g_autoptr(DzlSuggestionEntryBuffer) buffer = NULL; + g_autoptr(DzlSuggestion) suggestion = NULL; + g_autoptr(DzlSuggestion) suggestion2 = NULL; + const gchar *text; + guint len; + guint n_chars; + + buffer = dzl_suggestion_entry_buffer_new (); + + suggestion = dzl_suggestion_new (); + dzl_suggestion_set_id (suggestion, "some-id"); + dzl_suggestion_set_title (suggestion, "this is the title"); + dzl_suggestion_set_subtitle (suggestion, "this is the subtitle"); + dzl_suggestion_set_icon_name (suggestion, "gtk-missing-symbolic"); + g_signal_connect (suggestion, "suggest-suffix", G_CALLBACK (suggest_suffix), "abcd"); + + suggestion2 = dzl_suggestion_new (); + g_signal_connect (suggestion2, "suggest-suffix", G_CALLBACK (suggest_suffix), "99999"); + + dzl_suggestion_entry_buffer_set_suggestion (buffer, suggestion); + g_assert (suggestion == dzl_suggestion_entry_buffer_get_suggestion (buffer)); + + gtk_entry_buffer_insert_text (GTK_ENTRY_BUFFER (buffer), 0, "1234", 4); + + len = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpint (len, ==, 8); + + text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpstr (text, ==, "1234abcd"); + + n_chars = gtk_entry_buffer_insert_text (GTK_ENTRY_BUFFER (buffer), 4, "z", 1); + g_assert_cmpint (n_chars, ==, 1); + + len = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpint (len, ==, 9); + + text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpstr (text, ==, "1234zabcd"); + + n_chars = gtk_entry_buffer_delete_text (GTK_ENTRY_BUFFER (buffer), 1, 1); + g_assert_cmpint (n_chars, ==, 1); + + len = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpint (len, ==, 8); + + text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpstr (text, ==, "134zabcd"); + + dzl_suggestion_entry_buffer_set_suggestion (buffer, NULL); + + len = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpint (len, ==, 4); + + text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpstr (text, ==, "134z"); + + dzl_suggestion_entry_buffer_set_suggestion (buffer, suggestion2); + + len = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpint (len, ==, 9); + + text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpstr (text, ==, "134z99999"); + + dzl_suggestion_entry_buffer_set_suggestion (buffer, suggestion); + + len = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpint (len, ==, 8); + + text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpstr (text, ==, "134zabcd"); + + /* Fail by trying to delete the extended text */ + len = gtk_entry_buffer_delete_text (GTK_ENTRY_BUFFER (buffer), 4, 4); + g_assert_cmpint (len, ==, 0); + + text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer)); + g_assert_cmpstr (text, ==, "134zabcd"); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + gtk_init (&argc, &argv); + g_test_add_func ("/Dazzle/SuggestionEntryBuffer/basic", test_basic); + return g_test_run (); +} diff --git a/tests/test-suggestion.c b/tests/test-suggestion.c new file mode 100644 index 0000000..123ab2a --- /dev/null +++ b/tests/test-suggestion.c @@ -0,0 +1,320 @@ +/* test-suggestion.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#include +#include + +/* + * Most of this is exactly how you SHOULD NOT write a web browser + * shell. It's just dummy code to test the suggestions widget. + * Think for yourself before copying any of this code. + */ + +typedef struct +{ + const gchar *icon_name; + const gchar *url; + const gchar *title; + const gchar *suffix; +} DemoData; + +static DzlFuzzyMutableIndex *search_index; +static const DemoData demo_data[] = { + { "web-browser-symbolic", "https://twitter.com", "Twitter", "twitter.com" }, + { "web-browser-symbolic", "https://facebook.com", "Facebook", "facebook.com" }, + { "web-browser-symbolic", "https://google.com", "Google", "google.com" }, + { "web-browser-symbolic", "https://images.google.com", "Google Images", "images.google.com" }, + { "web-browser-symbolic", "https://news.ycombinator.com", "Hacker News", "news.ycombinator.com" }, + { "web-browser-symbolic", "https://reddit.com/r/gnome", "GNOME Desktop Environment", "reddit.com/r/gnome" }, + { "web-browser-symbolic", "https://reddit.com/r/linux", "Linux, GNU/Linux, free software", "reddit.com/r/linux" }, + { "web-browser-symbolic", "https://wiki.gnome.org", "GNOME Wiki", "wiki.gnome.org" }, + { "web-browser-symbolic", "https://gnome.org", "GNOME", "gnome.org" }, + { "web-browser-symbolic", "https://planet.gnome.org", "Planet GNOME", "planet.gnome.org" }, + { "web-browser-symbolic", "https://wiki.gnome.org/Apps/Builder", "GNOME Builder", "wiki.gnome.org/Apps/Builder" }, +}; + +static void +take_item (GListStore *store, + DzlSuggestion *suggestion) +{ + g_list_store_append (store, suggestion); + g_object_unref (suggestion); +} + +static gboolean +is_a_url (const gchar *str) +{ + /* you obviously want something better */ + + if (strstr (str, ".com") || + strstr (str, ".net") || + strstr (str, ".org") || + strstr (str, ".io") || + strstr (str, ".ly")) + return TRUE; + + return FALSE; +} + +static gint +compare_match (gconstpointer a, + gconstpointer b) +{ + const DzlFuzzyMutableIndexMatch *match_a = a; + const DzlFuzzyMutableIndexMatch *match_b = b; + + if (match_a->score < match_b->score) + return 1; + else if (match_a->score > match_b->score) + return -1; + else + return 0; +} + +static gchar * +suggest_suffix (DzlSuggestion *suggestion, + const gchar *typed_text, + const DemoData *data) +{ + //g_print ("Suffix: Typed_Text=%s\n", typed_text); + + if (g_str_has_prefix (data->suffix, typed_text)) + return g_strdup (data->suffix + strlen (typed_text)); + + return NULL; +} + +static gchar * +replace_typed_text (DzlSuggestion *suggestion, + const gchar *typed_text, + const DemoData *data) +{ + return g_strdup (data->url); +} + +static GListModel * +create_search_results (const gchar *full_query, + const gchar *query) +{ + GListStore *store = g_list_store_new (DZL_TYPE_SUGGESTION); + g_autoptr(GArray) matches = NULL; + g_autofree gchar *search_url = NULL; + g_autofree gchar *with_slashes = g_strdup_printf ("://%s", query); + gboolean exact = FALSE; + + matches = dzl_fuzzy_mutable_index_match (search_index, query, 20); + + g_array_sort (matches, compare_match); + + for (guint i = 0; i < matches->len; i++) + { + const DzlFuzzyMutableIndexMatch *match = &g_array_index (matches, DzlFuzzyMutableIndexMatch, i); + const DemoData *data = match->value; + g_autofree gchar *markup = NULL; + DzlSuggestion *item; + + markup = dzl_fuzzy_highlight (data->url, query, FALSE); + + if (g_str_has_suffix (data->url, with_slashes)) + exact = TRUE; + + item = g_object_new (DZL_TYPE_SUGGESTION, + "id", data->url, + "icon-name", data->icon_name, + "title", markup, + "subtitle", data->title, + NULL); + g_signal_connect (item, "suggest-suffix", G_CALLBACK (suggest_suffix), (gpointer)data); + g_signal_connect (item, "replace-typed-text", G_CALLBACK (replace_typed_text), (gpointer)data); + take_item (store, item); + } + + if (!exact && is_a_url (full_query)) + { + g_autofree gchar *url = g_strdup_printf ("http://%s", full_query); + take_item (store, + g_object_new (DZL_TYPE_SUGGESTION, + "id", url, + "icon-name", NULL, + "title", query, + "subtitle", NULL, + NULL)); + } + + search_url = g_strdup_printf ("https://www.google.com/search?q=%s", full_query); + take_item (store, + g_object_new (DZL_TYPE_SUGGESTION, + "id", search_url, + "title", full_query, + "subtitle", "Google Search", + "icon-name", "edit-find-symbolic", + NULL)); + + return G_LIST_MODEL (store); +} + +static gboolean +key_press (GtkWidget *widget, + GdkEventKey *key, + GtkEntry *param) +{ + if (key->keyval == GDK_KEY_l && (key->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK) + { + gtk_widget_grab_focus (GTK_WIDGET (param)); + gtk_editable_select_region (GTK_EDITABLE (param), 0, -1); + } + else if (key->keyval == GDK_KEY_w && (key->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK) + gtk_window_close (GTK_WINDOW (widget)); + + return FALSE; +} + +static void +search_changed (DzlSuggestionEntry *entry, + gpointer user_data) +{ + g_autoptr(GListModel) model = NULL; + GString *str = g_string_new (NULL); + const gchar *text; + + g_assert (DZL_IS_SUGGESTION_ENTRY (entry)); + + text = dzl_suggestion_entry_get_typed_text (entry); + + for (const gchar *iter = text; *iter; iter = g_utf8_next_char (iter)) + { + gunichar ch = g_utf8_get_char (iter); + + if (!g_unichar_isspace (ch)) + g_string_append_unichar (str, ch); + } + + if (str->len) + model = create_search_results (text, str->str); + + dzl_suggestion_entry_set_model (entry, model); +} + +static void +suggestion_activated (DzlSuggestionEntry *entry, + DzlSuggestion *suggestion, + gpointer user_data) +{ + const gchar *uri = dzl_suggestion_get_id (suggestion); + + g_print ("Activated suggestion: %s\n", uri); + + gtk_entry_set_text (GTK_ENTRY (entry), uri); +} + +static void +load_css (void) +{ + g_autoptr(GtkCssProvider) provider = NULL; + + provider = dzl_css_provider_new ("resource:///org/gnome/dazzle/themes"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +int +main (gint argc, + gchar *argv[]) +{ + GtkWidget *window; + GtkWidget *header; + GtkWidget *entry; + GtkWidget *box; + GtkWidget *button; + GtkWidget *scroller; + + gtk_init (&argc, &argv); + + load_css (); + + search_index = dzl_fuzzy_mutable_index_new (FALSE); + + for (guint i = 0; i < G_N_ELEMENTS (demo_data); i++) + { + const DemoData *data = &demo_data[i]; + + dzl_fuzzy_mutable_index_insert (search_index, data->url, (gpointer)data); + } + + window = g_object_new (GTK_TYPE_WINDOW, + "default-width", 1100, + "default-height", 600, + NULL); + header = g_object_new (GTK_TYPE_HEADER_BAR, + "show-close-button", TRUE, + "visible", TRUE, + NULL); + + scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW, + "child", g_object_new (GTK_TYPE_TEXT_VIEW, + "visible", TRUE, + NULL), + "expand", TRUE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), scroller); + + box = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "visible", TRUE, + "spacing", 0, + NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (box)), "linked"); + + entry = g_object_new (DZL_TYPE_SUGGESTION_ENTRY, + "halign", GTK_ALIGN_CENTER, + "hexpand", FALSE, + "max-width-chars", 55, + "visible", TRUE, + "width-chars", 30, + NULL); + dzl_suggestion_entry_set_position_func (DZL_SUGGESTION_ENTRY (entry), + dzl_suggestion_entry_window_position_func, + NULL, NULL); + gtk_box_set_center_widget (GTK_BOX (box), entry); + g_signal_connect (entry, "changed", G_CALLBACK (search_changed), NULL); + g_signal_connect (entry, "suggestion-activated", G_CALLBACK (suggestion_activated), NULL); + + button = g_object_new (GTK_TYPE_BUTTON, + "halign", GTK_ALIGN_START, + "hexpand", FALSE, + "visible", TRUE, + "child", g_object_new (GTK_TYPE_IMAGE, + "visible", TRUE, + "icon-name", "web-browser-symbolic", + NULL), + NULL); + gtk_box_pack_end (GTK_BOX (box), button, FALSE, FALSE, 0); + + gtk_window_set_titlebar (GTK_WINDOW (window), header); + gtk_header_bar_set_custom_title (GTK_HEADER_BAR (header), box); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + g_signal_connect (window, "key-press-event", G_CALLBACK (key_press), entry); + gtk_window_present (GTK_WINDOW (window)); + + gtk_main (); + + return 0; +} diff --git a/tests/test-tab-strip.c b/tests/test-tab-strip.c new file mode 100644 index 0000000..c574962 --- /dev/null +++ b/tests/test-tab-strip.c @@ -0,0 +1,29 @@ +#include + +gint +main (gint argc, + gchar *argv[]) +{ + GtkBuilder *builder = NULL; + GtkWindow *window = NULL; + GError *error = NULL; + g_autofree gchar *path = g_build_filename (TEST_DATA_DIR, "test-tab-strip.ui", NULL); + + gtk_init (&argc, &argv); + + g_type_ensure (DZL_TYPE_TAB_STRIP); + + builder = gtk_builder_new (); + gtk_builder_add_from_file (builder, path, &error); + g_assert_no_error (error); + + window = GTK_WINDOW (gtk_builder_get_object (builder, "window")); + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + + gtk_window_present (window); + gtk_main (); + + g_clear_object (&builder); + + return 0; +} diff --git a/tests/test-task-cache.c b/tests/test-task-cache.c new file mode 100644 index 0000000..a23df59 --- /dev/null +++ b/tests/test-task-cache.c @@ -0,0 +1,68 @@ +#include + +static GMainLoop *main_loop; +static DzlTaskCache *cache; +static GObject *foo; + +static void +populate_callback (DzlTaskCache *self, + gconstpointer key, + GTask *task, + gpointer user_data) +{ + foo = g_object_new (G_TYPE_OBJECT, NULL); + g_object_add_weak_pointer (G_OBJECT (foo), (gpointer *)&foo); + g_task_return_pointer (task, foo, g_object_unref); +} + +static void +get_foo_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GError *error = NULL; + GObject *ret; + + ret = dzl_task_cache_get_finish (cache, result, &error); + g_assert_no_error (error); + g_assert (ret != NULL); + g_assert (ret == foo); + + g_assert (dzl_task_cache_evict (cache, "foo")); + g_object_unref (ret); + + g_main_loop_quit (main_loop); +} + +static void +test_task_cache (void) +{ + main_loop = g_main_loop_new (NULL, FALSE); + cache = dzl_task_cache_new (g_str_hash, + g_str_equal, + (GBoxedCopyFunc)g_strdup, + (GBoxedFreeFunc)g_free, + g_object_ref, + g_object_unref, + 100 /* msec */, + populate_callback, NULL, NULL); + + g_assert (!dzl_task_cache_peek (cache, "foo")); + g_assert (!dzl_task_cache_evict (cache, "foo")); + + dzl_task_cache_get_async (cache, "foo", TRUE, NULL, get_foo_cb, NULL); + + g_main_loop_run (main_loop); + g_main_loop_unref (main_loop); + + g_assert (foo == NULL); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Dazzle/TaskCache/basic", test_task_cache); + return g_test_run (); +} diff --git a/tests/test-tree.c b/tests/test-tree.c new file mode 100644 index 0000000..699f504 --- /dev/null +++ b/tests/test-tree.c @@ -0,0 +1,241 @@ +#include + +static void +build_children_cb (DzlTreeBuilder *builder, + DzlTreeNode *node) +{ + GFile *file; + + g_assert (DZL_IS_TREE_BUILDER (builder)); + g_assert (DZL_IS_TREE_NODE (node)); + + file = G_FILE (dzl_tree_node_get_item (node)); + g_assert (G_IS_FILE (file)); + + if (g_file_query_file_type (file, 0, NULL) == G_FILE_TYPE_DIRECTORY) + { + static const GdkRGBA dim = { 0, 0, 0, 0.5 }; + g_autoptr(GFileEnumerator) enumerator = NULL; + gpointer infoptr; + + enumerator = g_file_enumerate_children (file, + G_FILE_ATTRIBUTE_STANDARD_NAME"," + G_FILE_ATTRIBUTE_STANDARD_ICON"," + G_FILE_ATTRIBUTE_STANDARD_TYPE, + 0, NULL, NULL); + + if (enumerator == NULL) + return; + + while (!!(infoptr = g_file_enumerator_next_file (enumerator, NULL, NULL))) + { + g_autoptr(GFileInfo) info = infoptr; + const gchar *name = g_file_info_get_name (info); + g_autoptr(GFile) child = g_file_get_child (file, name); + DzlTreeNode *child_node; + + child_node = dzl_tree_node_new (); + dzl_tree_node_set_item (child_node, G_OBJECT (child)); + dzl_tree_node_set_text (child_node, g_file_info_get_name (info)); + dzl_tree_node_set_gicon (child_node, g_file_info_get_icon (info)); + dzl_tree_node_set_reset_on_collapse (child_node, TRUE); + + if (*name == '.') + dzl_tree_node_set_foreground_rgba (child_node, &dim); + + if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) + dzl_tree_node_set_children_possible (child_node, TRUE); + + dzl_tree_node_append (node, child_node); + } + + g_file_enumerator_close (enumerator, NULL, NULL); + } +} + +static gboolean +node_draggable_cb (DzlTreeBuilder *builder, + DzlTreeNode *node) +{ + return TRUE; +} + +static gboolean +node_droppable_cb (DzlTreeBuilder *builder, + DzlTreeNode *node, + GtkSelectionData *data) +{ + /* XXX: Check that uri/node is not a parent of node */ + return TRUE; +} + +static gboolean +node_drag_data_get_cb (DzlTreeBuilder *builder, + DzlTreeNode *node, + GtkSelectionData *data) +{ + g_autofree gchar *uri = NULL; + gchar *uris[] = { NULL, NULL }; + GFile *file; + + g_assert (DZL_IS_TREE_BUILDER (builder)); + g_assert (DZL_IS_TREE_NODE (node)); + + if (gtk_selection_data_get_target (data) != gdk_atom_intern_static_string ("text/uri-list")) + return FALSE; + + file = G_FILE (dzl_tree_node_get_item (node)); + uris[0] = uri = g_file_get_uri (file); + if (gtk_selection_data_set_uris (data, uris)) + g_print ("Set uri to %s\n", uri); + + return TRUE; +} + +static gboolean +drag_data_received_cb (DzlTreeBuilder *builder, + DzlTreeNode *node, + DzlTreeDropPosition position, + GdkDragAction action, + GtkSelectionData *data) +{ + g_assert (DZL_IS_TREE_BUILDER (builder)); + g_assert (DZL_IS_TREE_NODE (node)); + g_assert (data != NULL); + + g_print ("Drag data received: action = %d\n", action); + + if (gtk_selection_data_get_target (data) == gdk_atom_intern ("text/uri-list", FALSE)) + { + g_auto(GStrv) uris = NULL; + g_autofree gchar *str = NULL; + g_autofree gchar *dst = NULL; + GFile *file; + + /* + * We get a node inside the parent when dropping onto a parent. + * So we really want to try to get the file for the parent node. + */ + + node = dzl_tree_node_get_parent (node); + file = G_FILE (dzl_tree_node_get_item (node)); + + g_assert (DZL_IS_TREE_NODE (node)); + g_assert (G_IS_FILE (file)); + + uris = gtk_selection_data_get_uris (data); + str = g_strjoinv (" ", uris); + dst = g_file_get_uri (file); + + g_print ("Dropping uris: %s onto %s\n", str, dst); + + return TRUE; + } + + return FALSE; +} + +static gboolean +drag_node_delete_cb (DzlTreeBuilder *builder, + DzlTreeNode *node) +{ + g_assert (DZL_IS_TREE_BUILDER (builder)); + g_assert (DZL_IS_TREE_NODE (node)); + + /* This is called when GTK_ACTION_MOVE is used and we need + * to cleanup the old node which is not gone. + */ + g_print ("Delete node %s\n", dzl_tree_node_get_text (node)); + + return FALSE; +} + +static gboolean +drag_node_received_cb (DzlTreeBuilder *builder, + DzlTreeNode *drag_node, + DzlTreeNode *drop_node, + DzlTreeDropPosition position, + GdkDragAction action, + GtkSelectionData *data, + gpointer user_data) +{ + g_assert (DZL_IS_TREE_BUILDER (builder)); + g_assert (DZL_IS_TREE_NODE (drag_node)); + g_assert (DZL_IS_TREE_NODE (drop_node)); + g_assert (data != NULL); + + g_print ("Drop %s onto %s with pos %d and action %d\n", + dzl_tree_node_get_text (drag_node), + dzl_tree_node_get_text (drop_node), + position, action); + + /* Pretend we succeeded */ + + return TRUE; +} + +gint +main (gint argc, + gchar *argv[]) +{ + static const GtkTargetEntry drag_targets[] = { + { "GTK_TREE_MODEL_ROW", GTK_TARGET_SAME_WIDGET, 0 }, + { "text/uri-list", 0, 0 }, + }; + g_autoptr(DzlTreeNode) root = NULL; + g_autoptr(GFile) home = NULL; + DzlTreeBuilder *builder; + GtkWidget *window; + GtkWidget *scroller; + GtkWidget *tree; + + gtk_init (&argc, &argv); + + home = g_file_new_for_path (g_get_home_dir ()); + + window = g_object_new (GTK_TYPE_WINDOW, + "default-width", 300, + "default-height", 700, + "title", "Tree Test", + "visible", TRUE, + NULL); + + scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (window), scroller); + + tree = g_object_new (DZL_TYPE_TREE, + "show-icons", TRUE, + "headers-visible", FALSE, + "visible", TRUE, + NULL); + gtk_tree_view_enable_model_drag_source (GTK_TREE_VIEW (tree), + GDK_BUTTON1_MASK, + drag_targets, G_N_ELEMENTS (drag_targets), + GDK_ACTION_COPY | GDK_ACTION_MOVE); + gtk_tree_view_enable_model_drag_dest (GTK_TREE_VIEW (tree), + drag_targets, G_N_ELEMENTS (drag_targets), + GDK_ACTION_COPY | GDK_ACTION_MOVE); + gtk_container_add (GTK_CONTAINER (scroller), tree); + + builder = dzl_tree_builder_new (); + g_signal_connect (builder, "build-children", G_CALLBACK (build_children_cb), NULL); + g_signal_connect (builder, "drag-data-get", G_CALLBACK (node_drag_data_get_cb), NULL); + g_signal_connect (builder, "drag-data-received", G_CALLBACK (drag_data_received_cb), NULL); + g_signal_connect (builder, "drag-node-received", G_CALLBACK (drag_node_received_cb), NULL); + g_signal_connect (builder, "drag-node-delete", G_CALLBACK (drag_node_delete_cb), NULL); + g_signal_connect (builder, "node-draggable", G_CALLBACK (node_draggable_cb), NULL); + g_signal_connect (builder, "node-droppable", G_CALLBACK (node_droppable_cb), NULL); + dzl_tree_add_builder (DZL_TREE (tree), builder); + + root = dzl_tree_node_new (); + dzl_tree_node_set_item (root, G_OBJECT (home)); + dzl_tree_set_root (DZL_TREE (tree), root); + + g_signal_connect (window, "delete-event", gtk_main_quit, NULL); + gtk_window_present (GTK_WINDOW (window)); + gtk_main (); + + return 0; +} diff --git a/tests/test-trie.c b/tests/test-trie.c new file mode 100644 index 0000000..88b0f47 --- /dev/null +++ b/tests/test-trie.c @@ -0,0 +1,139 @@ +#include + +static void +test_dzl_trie_insert (void) +{ + DzlTrie *trie; + + trie = dzl_trie_new(NULL); + dzl_trie_insert(trie, "a", "a"); + g_assert_cmpstr("a", ==, dzl_trie_lookup(trie, "a")); + dzl_trie_insert(trie, "b", "b"); + g_assert_cmpstr("b", ==, dzl_trie_lookup(trie, "b")); + dzl_trie_insert(trie, "c", "c"); + g_assert_cmpstr("c", ==, dzl_trie_lookup(trie, "c")); + dzl_trie_insert(trie, "d", "d"); + g_assert_cmpstr("d", ==, dzl_trie_lookup(trie, "d")); + dzl_trie_insert(trie, "e", "e"); + g_assert_cmpstr("e", ==, dzl_trie_lookup(trie, "e")); + dzl_trie_insert(trie, "f", "f"); + g_assert_cmpstr("f", ==, dzl_trie_lookup(trie, "f")); + dzl_trie_insert(trie, "g", "g"); + g_assert_cmpstr("g", ==, dzl_trie_lookup(trie, "g")); + dzl_trie_destroy(trie); +} + +static gboolean +traverse_cb (DzlTrie *trie, + const gchar *key, + gpointer value, + gpointer user_data) +{ + guint *count = user_data; + + (*count)++; + + return FALSE; +} + +static void +test_dzl_trie_gauntlet (void) +{ + g_autofree gchar *path = NULL; + gboolean ret; + GTimer *timer; + GError *error = NULL; + gchar *content; + gchar **words; + guint word_count = 0; + guint count = 0; + guint i; + guint j; + DzlTrie *trie; + + path = g_build_filename (TEST_DATA_DIR, "words.txt", NULL); + ret = g_file_get_contents(path, &content, NULL, &error); + g_assert_no_error(error); + if (!ret) { + g_assert(ret); + } + + words = g_strsplit(content, "\n", -1); + trie = dzl_trie_new(NULL); + + g_free(content); + content = NULL; + + g_print("\ninsert,read,traverse,remove,free\n"); + + timer = g_timer_new(); + + for (i = 0; words[i]; i++) { + dzl_trie_insert(trie, words[i], words[i]); + } + + word_count = i; + + g_timer_stop(timer); + g_print("%lf", g_timer_elapsed(timer, NULL)); + g_timer_reset(timer); + + for (j = 0; j < 4; j++) { + for (i = 0; words[i]; i++) { + gchar *s = dzl_trie_lookup(trie, words[i]); + g_assert_cmpstr(words[i], ==, s); + } + } + + g_timer_stop(timer); + g_print(",%lf", g_timer_elapsed(timer, NULL)); + g_timer_reset(timer); + + dzl_trie_traverse(trie, NULL, + G_PRE_ORDER, G_TRAVERSE_LEAVES, -1, + traverse_cb, &count); + g_assert_cmpint(count, ==, word_count); + + g_timer_stop(timer); + g_print(",%lf", g_timer_elapsed(timer, NULL)); + g_timer_reset(timer); + + for (i = 0; words[i]; i++) { + if (i % 2 == 0) { + g_assert(dzl_trie_remove(trie, words[i])); + } + } + + for (i = 0; words[i]; i++) { + if (i % 2 != 0) { + g_assert_cmpstr(words[i], ==, dzl_trie_lookup(trie, words[i])); + } else { + g_assert(!dzl_trie_lookup(trie, words[i])); + } + } + + g_timer_stop(timer); + g_print(",%lf", g_timer_elapsed(timer, NULL)); + g_timer_reset(timer); + + dzl_trie_destroy(trie); + trie = NULL; + + g_timer_stop(timer); + g_print(",%lf\n", g_timer_elapsed(timer, NULL)); + + g_strfreev(words); + words = NULL; + + g_timer_destroy (timer); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init(&argc, &argv, NULL); + g_test_add_func("/Dazzle/Trie/insert", test_dzl_trie_insert); + g_test_add_func("/Dazzle/Trie/gauntlet", test_dzl_trie_gauntlet); + return g_test_run(); +} diff --git a/tests/test-util.c b/tests/test-util.c new file mode 100644 index 0000000..724102f --- /dev/null +++ b/tests/test-util.c @@ -0,0 +1,72 @@ +/* test-util.c + * + * Copyright (C) 2017 Christian Hergert + * + * 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 . + */ + +#include + +#include "util/dzl-util-private.h" + +static void +test_action_parsing (void) +{ + struct { + const gchar *input; + const gchar *expected_prefix; + const gchar *expected_name; + const gchar *expected_target; + } simple_tests[] = { + { "app.foobar", "app", "foobar", NULL }, + { "win.foo", "win", "foo", NULL }, + { "win.foo::1", "win", "foo", "'1'" }, + { "win.foo(1)", "win", "foo", "1" }, + }; + + for (guint i = 0; i < G_N_ELEMENTS (simple_tests); i++) + { + g_autofree gchar *prefix = NULL; + g_autofree gchar *name = NULL; + g_autoptr(GVariant) target = NULL; + + if (!dzl_g_action_name_parse_full (simple_tests[i].input, &prefix, &name, &target)) + g_error ("Failed to parse %s", simple_tests[i].input); + + g_assert_cmpstr (prefix, ==, simple_tests[i].expected_prefix); + g_assert_cmpstr (name, ==, simple_tests[i].expected_name); + + if (simple_tests[i].expected_target) + { + g_autoptr(GVariant) expected_target = g_variant_parse (NULL, + simple_tests[i].expected_target, + NULL, NULL, NULL); + if (!g_variant_equal (expected_target, target)) + { + g_printerr ("Expected: %s\n", g_variant_print (expected_target, TRUE)); + g_printerr ("Actual: %s\n", g_variant_print (target, TRUE)); + g_assert_not_reached (); + } + } + } +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/Util/Action/parse", test_action_parsing); + return g_test_run (); +} diff --git a/tools/dazzle-list-counters.c b/tools/dazzle-list-counters.c new file mode 100644 index 0000000..4f88335 --- /dev/null +++ b/tools/dazzle-list-counters.c @@ -0,0 +1,110 @@ +/* dazzle-list-counters.c + * + * Copyright (C) 2015-2017 Christian Hergert + * + * 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 . + */ + +#include +#include +#include +#include +#include + +static void +foreach_cb (DzlCounter *counter, + gpointer user_data) +{ + guint *n_counters = user_data; + + (*n_counters)++; + + g_print ("%-20s : %-32s : %20"G_GINT64_FORMAT" : %-s\n", + counter->category, + counter->name, + dzl_counter_get (counter), + counter->description); +} + +static gboolean +int_parse_with_range (gint *value, + gint lower, + gint upper, + const gchar *str) +{ + gint64 v64; + + g_assert (value); + g_assert (lower <= upper); + + v64 = g_ascii_strtoll (str, NULL, 10); + + if (((v64 == G_MININT64) || (v64 == G_MAXINT64)) && (errno == ERANGE)) + return FALSE; + + if ((v64 < lower) || (v64 > upper)) + return FALSE; + + *value = (gint)v64; + + return TRUE; +} + +gint +main (gint argc, + gchar *argv[]) +{ + DzlCounterArena *arena; + guint n_counters = 0; + gint pid; + + if (argc != 2) + { + fprintf (stderr, "usage: %s [PID | SHM_PATH]\n", argv [0]); + return EXIT_FAILURE; + } + + if (g_str_has_prefix (argv [1], "/dev/shm/DzlCounters-")) + argv [1] += strlen ("/dev/shm/DzlCounters-"); + + if (!int_parse_with_range (&pid, 1, G_MAXUSHORT, argv [1])) + { + fprintf (stderr, "usage: %s \n", argv [0]); + return EXIT_FAILURE; + } + + arena = dzl_counter_arena_new_for_pid (pid); + + if (!arena) + { + fprintf (stderr, "Failed to access counters for process %u.\n", (int)pid); + return EXIT_FAILURE; + } + + g_print ("%-20s : %-32s : %20s : %-72s\n", + " Category", + " Name", "Value", "Description"); + g_print ("-------------------- : " + "-------------------------------- : " + "-------------------- : " + "------------------------------------------------------------------------\n"); + dzl_counter_arena_foreach (arena, foreach_cb, &n_counters); + g_print ("-------------------- : " + "-------------------------------- : " + "-------------------- : " + "------------------------------------------------------------------------\n"); + g_print ("Discovered %u counters\n", n_counters); + + return EXIT_SUCCESS; +} diff --git a/tools/meson.build b/tools/meson.build new file mode 100644 index 0000000..a64a152 --- /dev/null +++ b/tools/meson.build @@ -0,0 +1,11 @@ +if get_option('enable_tools') + +# Counters are UNIX-systems only currently +if host_machine.system() != 'windows' +dazzle_list_counters = executable('dazzle-list-counters', 'dazzle-list-counters.c', + dependencies: libdazzle_deps + [libdazzle_dep], + install: true, +) +endif + +endif