pax_global_header00006660000000000000000000000064151223352000014503gustar00rootroot0000000000000052 comment=7a34e96f3a0abb55bc842a266bd152f59bcdee14 dangvd-crystal-dock-7a34e96/000077500000000000000000000000001512233520000157075ustar00rootroot00000000000000dangvd-crystal-dock-7a34e96/.gitignore000066400000000000000000000000061512233520000176730ustar00rootroot00000000000000build dangvd-crystal-dock-7a34e96/LICENSE000066400000000000000000001045151512233520000167220ustar00rootroot00000000000000 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 . dangvd-crystal-dock-7a34e96/README.md000066400000000000000000000060461512233520000171740ustar00rootroot00000000000000![Crystal Dock](https://github.com/dangvd/crystal-dock/raw/main/images/crystal-dock.jpg) # Crystal Dock [Releases](https://github.com/dangvd/crystal-dock/releases) [Documentation](https://github.com/dangvd/crystal-dock/wiki/Documentation) [FAQ & Troubleshooting](https://github.com/dangvd/crystal-dock/wiki/FAQ-&-Troubleshooting) [Author & Contributors](https://github.com/dangvd/crystal-dock/wiki/Author-&-Contributors) Crystal Dock is a cool dock (desktop panel) for Linux desktop, with the focus on attractive user interface, simplicity and cross-desktop support. The current version (version 2) supports Budgie, Hyprland, KDE Plasma 6, Labwc, LXQt, Niri, Sway and Wayfire on Wayland. Other desktop environments and compositors will be considered when they run on Wayland and provide sufficient APIs. The previous version (version 1) supports KDE Plasma 5, GNOME, LXQt, Cinnamon and MATE on X11. ## Main features - Smooth parabolic zooming and translucent effect - Four visual styles: Glass 3D, Glass 2D, Flat 2D and Metal 2D with various appearance settings - Supported components: Application Menu (Application Launcher), Launcher/Task Manager, Trash, Wi-Fi Manager, Volume Control, Battery Indicator, Keyboard Layout, Version Checker, Clock and (on some environments) Pager - Multiple docks support - Integration with various desktop environments / compositors: specific default launchers, special menu entries (e.g. Log Out) - Separate configs for separate desktop environments / compositors ## Icon theme Crystal Dock simply uses the system icon theme. The one shown in the screenshots is Crystal Remix icon theme: https://github.com/dangvd/crystal-remix-icon-theme ## License Crystal Dock is licensed under the GNU General Public License v3.0 ## Dependencies Crystal Dock is written in C++ and depends on: - Qt6 as the GUI framework - LayerShellQt6 for Wayland's Layer Shell integration --- ## Build from the source code ### Build dependencies Dependencies development packages: to build from the source code, Qt6, LayerShellQt6 and Wayland development packages are required. For example, to install them on OpenSUSE, run: ``` $ sudo zypper install qt6-base-private-devel wayland-devel layer-shell-qt6-devel ``` To install them on Fedora, run: ``` $ sudo dnf install qt6-qtbase-private-devel wayland-devel layer-shell-qt-devel ``` To install them on Ubuntu, run: ``` $ sudo apt install qt6-base-private-dev libwayland-dev liblayershellqtinterface-dev ``` ### Build commands To build and install, run: ``` $ cmake -S src -B build -DCMAKE_INSTALL_PREFIX=/usr $ cmake --build build --parallel $ sudo cmake --install build ``` After the installation, Crystal Dock can be launched from the Application list (Utilities category), or from the command line: ``` $ crystal-dock ``` Note that on KWin, Crystal Dock needs to be installed in order to be able to access necessary KDE Wayland protocols. To uninstall, navigate to the cloned repository and run: ``` $ sudo cmake --build build --target uninstall ``` To execute the automated tests, run: ``` $ ctest --test-dir build ``` dangvd-crystal-dock-7a34e96/images/000077500000000000000000000000001512233520000171545ustar00rootroot00000000000000dangvd-crystal-dock-7a34e96/images/crystal-dock.jpg000066400000000000000000021273511512233520000222700ustar00rootroot00000000000000JFIFHHC     C   ( |g Ϡq9%ոӶv5n05Z{w_T[NSɪ3rQŜ~MSv~*7u^můU,۩N!n3^Au:ox|(ڴKh +* s! j- vKhT$ T*tQitʋl *DrAh AK+K"YFU%܋ ض ]vz+bE@Yvvu E/4cjE|1Z}K^R]<zguotqqӽdqr5y8*Ӯ6]~UUQu`HE` T!@JR!bhtD@@Ֆ@eR4U-T]* KCEа\D[DDXA2)d[bF!#+,QlJ*Wf/׬Io*6t4e)4sݯ?KdGz\\͝=Gw[>usL&o 2x|2W;^YK8u_u̙}ea^Mpo|uk~sv6.o7.OV>w]o<_}6\qSly&o]nUZHE`!-B -EhC@M C0Rdl(*%Tȶ BURQtZRA`"X$[h\l-"PXR.Ab%TS|zOYѼ5>¬MIKI_<}o;]|ͧwDZ7%]C=g|ojΓѶkzSO/5|ws˟-pfu|8t~_oW7-QXR!l% 5d--*U h%(ВBYEPR,EUPX- ZPP@IldX5*2Ar!hR.R(QJ7[SbrpkOjlYUMm< >sw8̌km{v>H[Ո\QmnM Xܳo妞qy{H>'~K#k5lb{íC~=_yy\w迟t>k9Nk}Wug/#ߒyKorHj@B!5UE@E(jꈂa" A,[` hZ("lEAb,+ dP E%AY PXAR2$J* HQ( EKNNWg֮u}zfj+e]6ӂk]v.Lpz[L%W%Z*6X6ٕkmPjasqBJUBKPjTT* - R҄ d%,%*R-ZZ,PR(%PA`E$E Qš"RQ-1"*J)QiPX)kpW bWҿkvS+Jж-W84ݎSZtǃɥ]c*UjZ۩^ժ5380&VQWCL2p;'3mk{ٯdڭD(ͻh2T!:4-@PE@к(KJJ ZP Ae-H--%-Š% dX)n@ 12 -"(K[,R0ABB(,ҁsF]wcFƧӟ 1*TSu&iկ|56\4j*ץ;)Q:qe.B%?%kqœEU k7Vbժ+ @'ug?s5p?:꭪Z݉ft`M)B h KE ! U 6hQbԁjh"j*IB" `* QEbE(EpRAB`R&"2iYh #+dH(Xl)nSX;l>^~O\(u[5^U;jUs^25͛1y9MSav5MSM9\'V >yָxlE{]85!ֆͷ'̤l UP,PYPPU PJ[`ATUѠt1P( KtRb,[T]KAB4"(Kru"- eP nQbE  RP2(Pe.1ǭuuzuFjIjUu^uNzv.&:;W0i֙[SK^]Umx_%ưR߅dg[2=//|ky}ݯO8=-gyOos|CM![!i>_gWxm j-U$]ih AnZ 4`T@Ш(-(*P-@P"Ҥ@ fPPDX ["XPPA`P\$RnEb.]s98oYR+Һ]DH4vWq`g7 5E(:K35=ƒWwkgȖh;\>g=}Om.FsמI~|}:>s;vWo_37|oytsɕh~[n/1/W7&!8y!bДA@eB AaVP WEPB,H@! ERh KKh %[а !(\ йX[&AA2*, l \EY `C89>EUKU U]- ׋p6\;;vQ~]̼fp-U O& &_q{zf nh>i۟~f[bd^O~㱻Wy?uGc_gN9?oFfd˝=M76ZG3q?}QEl@ l@N!AVЂ+C@% j BJPP"E-- P(-$EH5%r,T]@-]fh$,PIEEXXsYi%M+YS@-ӓE[~Waw1/k|ٜŒ U t~}/B򻻮.ۃ':rNg& 2էyK%xw>W]O俰=NtxdIxRbyY]l;}|0;*DZ瞷z}&3xU~+/ɿQ fdu_A_~s}ܿy't]ɿK9z:\ϟ>䰸}r_=z<&ye -[@PUՈAnB RVմ"ZE,@)*! RJP Kj* `@Ű"ȐE@t - ,[Jв22( R"Ź #)`q}R*Jת[2+snͷ 3lܛ#>m[kmdqrjo{?rؘ|=/g<<{|W\ ]7^v?Dߥfgc?~?߱~Su=?Mt'y|[o,]7:>{=<#[w>sCݏ:ϥ:}/i/9.ںEeB@Tt((BQ (E,P "Ar"RTEZ -`P2 `ERE[T dX>_4*CřWEkal>3;iUIOk~箛{As|x[Ϣ;v8Y|syAv&>Z-ǃ_4/7s~nqS?7s S8aw|k~sߧ NCW_R_潭emzo{$M'K3^y?K (%BD@UT,-*A PZQiijRlZlT"H Hl ܭne*+ d$-EY\f"-Y_u%V cDl-ήj̜|}=w:N\i͍95gm~#_'gޗGwvn?&Qy>O~y}\~I~_Ov}N_e]Acf& Wwzo1:_=n5ߵ]_fpl}x5DeB2j@% uV"A j &4R( "-Š`H 5ET 1t+2AJ `D [H- -neYfh7=JK+e ۍ]v gFϷLо ]C>SÁa7^Oa_ؚ>CG.~M_jl>9̓tO eozo9˗OL|F'|n`yCSy:/\\]x>%s_~{y<w>zc?"W}=_7/<}{[_}w^S?7o^k< UǏ>;q=>o7Njn?.F3w/76T=u璽D}c^/xLwMh:~ދ;Oy 酫f{~Q'we|{;KxG @zo{< ɢnټhMv9t|\y/_}eXYx; Kg_|=${~y?[z}>w+[de*\KdRAj4funm柷A=Vw߬fԮC7S6ܓ ]S ~hǚ.-qrt=WQ_sW|񴊽oGvp:[|劣-k<%:"F0w9qc;|l}/|}+AOw9^[2 ^?<_0Rӕz???VKy:ρ[.=7{ߧ|gJӓo~6:s }߿COEZ(2MeY Hϥ>C]+P Ok=~__}?ls8=O N`vN[EʋZ[/?.WCX~s-_HbdY9t3bfo=_2?|Xn~+L/sǣH;x3}|~84]^kʱ_⾡޷k,^Wt.ki{{?z^n9I{KJkӼoC_G[ߏ|ޔXu;]z|Nj~{f~7'oynu|ߖI,O[g꧋uo~NCy>'q:]-7>/k} {^Υ.\S~oK=9gd?.pc9>n'N>LG=OΗK'{{?F98vzpv:Sg8x_7K=z4gBT.EAo!+#JEhW> 6q~_w7`}l=[Ou,zo}-bMWyk>7!|kK~cg&u߆~O+?MzGkƇ?软BW<'WOpO^y~cx:.~OLs߻mzG/CW󴫄nM_.OzƖ={dMǙ>sLgͷ|zK>@Eu< v}6Y.xs3n%N{Oh @Ȳ2 YYIR$_Y<v\GWtV++ǾwmFYEZZYqǗjko~a)}v[rq\/G_˞=O}5|~ (BxI8q]?aYw'O:[na9%>q񎷦)SxmGulb<=-M v?B 8_m_sы%){ֺwqꗃɧ7>UJz?8׏,ι/ڪI`dPenB A2- HT6?4XWd9p&:3vHCR"T((E  x2,IeeYdWwy-;eiuqym&VxnDY cѥPAD%Jw' Xx^cl d\w n9qWt-ɷwH3'0K.}Qqsdix Pd7{ˣllk],ɴ 9e3d\[psrv[ X($XDn8|[gjuzk(ra Ir \]PDHISR$PQYg]1X;HaRh@K"J%FX * ղDy1y9+8F^feD8^Ą3JT!_eqfy̢EX;1lU\urz}ˌm^* $Y16*%Ptӳ]kZD|ozȱd¤ OjP"ɘE.Nj"a, E"Z,!%mziȑ hX@T$ 'w>o|rI|/GlY;yt Ba$"B!\?cħ}n!yT}~yk"D B(DA@IB(X@  ؀ @ Y?peA'یckWoFZ94צ'6s0-G &YSVVlշq\~Mc?%|UEg?/#I^n2(kQ.2fqN2x.N\}rrlx,9X4, ET HRhH B@ @XPQ`&)hRġh " MEK`ZP AB- X,T  @X24\X*nN,>^AA{o}ݙܛUMcnj\/[;=S/V=qi,IVO&o6[7r͏_ fi52WoħË;5>^Vn]m:\ppf`syy_9ZtD jڢk@!.!B2u@tUj(j$ RՀXhFk@(h)tQ@ T#@ *R܋ `EԈ)`YZXZAFPXf09Ou+IuUV-We\}??U1WO'ǩgn2oXu*VN=VTE-Ց U)[4 5Ůȷk:V6mm{Mݎv|mzZoQy89'Rq1̢J "H@Մ!-U f4PP*E]UH BBVd4 BLh\( u@)5)4QE X,QY]f -K2]#!"*.EtVTSUyk O; խ^jJʣN _'Osz~&W25j/X|44r1|xܚi5p>7>߼iZVͲ)̌[$Zǽ~sk933Y\d 38VEMU݌>Ujսqo/ys<;no Nwcz]sSs͋sNl,||;iw=Nm7Q9Gfn:~[e_?k7P@B&h!J( ,."Zj@P ZPE.AiX5RA QPʢ2XE&ju]_c^+诏4fjj.OםuuW2.MifPUҳUmMr(ZWQlvFnMfpy\1j}l:m,VW-ְ{k'7-<9_Bd{'<b92.rx}{|gxܽ7;S/xG\A/c3$hi_&[!7a|{8guBnI%@@&)@Bj JU@ bBtihhDȡ@@X5("Bl ]eP E-BYY!nfAE% 3í`pEL R JE|2+k⻼.~dՌufe۱7Nvg w^Gal8F9k8~K$l.tH~_|>ϙn5n9:O/ܻ7yzyl$:M>EᇙG˻5kW M @CH@j*UTHH P.BER Kb+AbDZ "Y[T,]HҢ ,  d#Y,+^?A\ZA-a-ŷ:+sm[hbUVgm`SɶJx<+mzY}o-h)XIHX4>VKf-ٷenVb+ o/1qy5׫.<>nj7cowwkwXSv~WOÙ3>_?aݼxғ]ӫ7N_UY>z/|ߚ}+yWoxޏ柜|?3ߚ}GEozhҪdvzw|?f4dkP%*]h )PlPA-R@ZeRHE%.L5+"+` eiYr4Al 5"TE|^* Դdfm[n +da}3.6 ?J ?}K|o}潝o 7c59𽮗,vff/4_9c_|/.Ŭaޗ/ lOSYsៜ[~?V}[=O+coIًB=ϔ<ټ8㼏[Ⱦ/hHe@I& ٢Q@@!V$* J.@@ )llACY t)XQtP\ V! ,Vf(Z0Ib4 ]EWj%$A4XϔݾԌnߒw:N\lP޻Γ~Ǿ^~='#s3w<7cǬk#:=.t4|3ok<%1}%>='˻^c]z_Mꟗ:/O|HS³NW7''*wx};eb_K>جn>쮿=Qcqyz`KL@@-j%IlP " `X"R$Y`I` PIX( @QH2,(A@"S~ d\C#pg#@ѿ)VY^ U'98iĖ0I^S͍LioWlY]?-cY<<kv~ww_í\oŘ'1>kBxCt~2?Q/:cA?俬uw7}O9|:L[~~///'Ky'?iw;y͖N=s7JeΥ__$][zPJ@E!MnBX-Yj@P[2uŇF hH^p"i!jZE-ȩ @3/WP FV`>SBURɦ6qx[}]5c+G)g?ǯ`V0q{}7|(5-~_&syͷSt|{c[Wv2c7|_wu_CXω>ϥޣ>tQrϗO|C^%??Z7IJo_w_+އ[^٧[͊Zg&>>q3/3 sW.'guIyPV/69Wtg8וUc㳰_op|Ûڷ௵ŨzGs3y9B7 yŞUx^GzߝY, {tu'/%Or_"߫dwϧ[ĚM//}a}?oEw{__ms933Թ_Ѿ%\yHx^}<'WOpO^y~cx:.~OLs׽K_cҮ6Gbr}4["mn}wcWϛORL>G^Qmkz^6O9S_=NF[tn~isax~bj #+rh )b A"/x\:qY^gM- r5+Άh Svu\ٳ.ps9JB!NyGW89]J{7s6su5Menq%B^5O?[۳SB k#xOKspn]~f-a ~)^چ<>k,t~,'}aJ7~wܜx=~ ?Q~ůabMxG1;!OAxI8q=?a%ܽ>&v{m_M=!%q񎷧]U/q5 ~[9w W{KǪ^&rUO[qx ~ey=ylx ݈.E`d"5O>Gل@DD T {\uKkB@ B( )͍LDKA(D$͌יMk{[v3ˤ֌d&p0z( JR^x_3)_fǓK"שΟYNyHC3+ޟ\u{ֽqB2ozsqvȭe.rm3 `疵hqk87ӋX\ Dl@'[x HM[ئc/iB BʟSEPBe%TY!xcu= iE!l@?sq:cI7du#֤mѽs^ns T$K!T wOׂ+y,^b@$X K[,DA  `( ,* ,T"@$EԀ% 2xT0Kfo*AI'?-Rd$+x[-($ %,@ZP d Œl*!Q.[7B *("B$ E) %@ !J(bشR@P! A ,D$AlP@ E `H@P BX "bcz`Ad@@ υ Ϡq9%ոӶv5n05Z{w_T[NSɪ3rQŜ~MSv~*7u^můU,۩N!n3^Au:ox|(ڴKh +* s! j- vKhT$ T*tQitʋl *DrAh AK+K"YFU%܋ ض ]vz+bE@Yvvu E/4cjE|1Z}K^R]<zguotqqӽdqr `rHҩkzM[qˑǗÏ˳u19v,K98|&Gol;՗qLJQUuT]XX-B9ԨXh-*4]."B5eYAB-԰(M@ @,JDRE,+Q@,hPeA Y(X@HK[مét9!. M]MJEM(\k7W3gOxzQϭ${/h"jL6_LNWan6|񾛤-bۛ~`9s|/w8.^_Gx^K?=ƻ}C(5pu_u69IAs^KjQ!@T!@, ZE 40- ]I-,S"(* VJ.QEjR-K`@RPelrX @BA`EHDc! QL}=gcF +CM5%-&e~8pwuw16;y77ǪXܕv^IC.7+sv8s)֌ӿ;HggW_ugotg\7/}Kwo{oi]Jxqrp~7<:sQe'&9o;|;u`EV!@[ AE  Y K@ tB@Z AbD *,E$PhQThAK(Uu.BA`@ R[* AJ P\ZAd f TRc_z+`ZŚ-VUe[~#|}7O#yO>b>nW[e[BV7,q9i{Gtο~we85yǾ^'7N.Mwzq`u?=?;__~g6G؜7_rpy7_\/?Ggb^=ηu{Ƴp+O+FPB! - *)EE[WTD[ Z bHT B@! `( @-(`X]")* ȰPIJ "PPXD.DA(\rp=L~w+4V.X*q67kZN.krg#տzf(֨f * V]\9M5]4~]O;Ok>O ;];$~i_y>M=-t.{V]^cOˢø ǧY伯G}O?vvn 5*!J  (PU%HV)IAeT%T[E`-(PKd"AArKFTQl*AHddX"Աh"蠹VhƷqȯCu&f]k|??_W}e#2oFf28VO&\M8bsrѵݏ_'tz\j3q%ZUlm+fۅQ yGyݏ{ΆBJUBKPjTT* - R҄ d%,%*R-ZZ,PR(%PA`E$E Qš"RQ-1"*J)QiPX)kpW bWҿkvS+Jж-W84ݎSZtǃɥ]c*UjZ۩^ժ5380&VQWCL2p;'3mk{ٯdڭD(ͻh2T!:4-@PE@к(KJJ ZP Ae-H--%-Š% dX)n@ 12 -"(K[,R0ABB(,ҁsF]wcFƧӟ 1*TSu&iկ|56\4j*ץ;)Q:qe.B%?%kqœEU k7Vbժ+ @'ug?s9s jԫj4bY>m0@d#BtW`N&O-ķ1.<3NI=쯜{&kCfۓfRVS 6MRH*Y(,(@ BZ * (@%-Ъ PhJTABP)EuT-.ʠrJ@[dEIr2H("@ ME)((2:~: #5T@*RjfCM[]]ꝫx|LgZ@asr-|y\YU.?;Ƽ?7Ȳ3 ٙns:??'xn]x=O_5/{o@`HVhħƤ)V,Aj K@,! uTRUE@ AEXiETRAl*"k0%Ź"  2 r(3u7}8*ʔ]WU^"A+>ǯٸ\ɪ,uxG.֢YݷN4Tro_|y^s|7^2xnAr3qʱ?D|/;{>={淖ωǼz|$ +nQ;=9~ }UEE hJ]l KD( RTVTA@$ ")hRʴ-hX.eQh\-QP [ A\YJ.E "k!}yP|?Ң kŸ]~.Gǝ(?.f^38pX/8&OϽ-Գ[͎[~bf[bLO>K]|?[oW۽fqs캝 y޿|>zLlߺM{ߤ:Zt;\gscu+BFEKA%:*[B T *.dE )ABЊ 4ZQ@[ # ԗ PPʋ`PX"uZ*u"KP@@ % `fc]kf4QeMKNMenm_]vĽ fpcVg_H.//#T-}w~E?t;n]g7lsqh{Wo<̉./oAo?@~=ZjJ*hE(Y-eHRTB E CA@Ub`E M@J[PX +Bde(3`ehP .Er(,FSx?=UeUh, 4enVݛnf-6F5i9{|ڷ$۩z)uv;c~BԻA 6Nϳ>GO᮪]dqko7]ߗt^WMg?/O^_[qp~z=9*y3}7ヌsLk|FA,. ( ZU֖`hP" ! ҋKKR bE%b ,]DPX5.@``Ph s,QQYV! nf(Ƞu#1̱lϢOӯQ*FUE #edal<6fٓ )ߏa_$t(n"ﻝ/|~qhts}3yׯsv1|~-fɽ5#9}[W?.oso?櫶7'7]ާ~179--Pw<5~_v90:?ߠ'[zw_Q^(Wqy^O6 >34,%It*(ZYjР FAPY-AB( B dEEK(X(5`Ƞ-̥"@ e@H-J#VR2A34 Z}v%ڊZmyo xVsbW|{5Osk] po&?o]?1c;?2+Aߋ[kv\ڮn.SsS݈{ۍ}s uoK^ר=/|Wn~zet7^@nx7z~9x~|.ηyں[.W?w>˓o?b=ora*@% " P%(ZB h*JDH * ,  @* KYOk_j"!AY?=j5+U@:|{\lg?~Oy7zoyg}5^kt:_= u:ޯ{PG<:3^ߏގh .-\wOо?˭p]tVT7ru^g AsGxnVT[m@oQ5糏IKd6bv(=Ϣoƿ˚.bn<= { A@APX*JH-D$)eRBE v(2FRʁOK++.TL6k[/6{~6|,?+_NjUr?Gз_k\!o:Ipo񟇎c=I~=3-Bzݾ/uu濏>Wsn^0i|C]+P OK}_|^ l>_'<L'cM+RgY7}'!lߥz/[J}l[h^\>;q\s>7λNQu[9{But<_uR:oﹸtKŋV Ur_;Nyבɥ]s)5vAt^_3z/W|S^$O|>|4jcYvqk=No8PX]Mǣ{yG֋G&4N̩yx}/\D~yƛo;izv:=}C18[ayIz/G~ܘt{O}GGO/}7on[u]?za<>K86=߳Ԫ';kD=)kwsΖ=>[Q=S^>Frkyx>.]_gBqu3&~NXοkj}8Y}r=W<\yI׺c¹7/qh25"X.T#몍ed%Hhlܟ__GM/Sǵ?rSXs^vodyЋIl~NLlΟgqh8)*S/v=z?c:<@ <~_!s=>[Q7iz݊uãw;}3[ :%D] =\Ǟ^O.tsץg\tx&ӏɼ.~ `dPenB A2- HT7#~u}m8{=pO-7?ƛ^~pm.gMuux 4P8ߝgL ?C_rrw~M7` >m}\ +׳gs[˓ =1+η?{` .B >j * ><$BJk ,$dz!Wh)C6T lH7#g}bykZqhޫ̺֬g&pۜ PT!B*@v|=?|YUIgsmWAa"-RP- +T% HDA%CB%C0j@I %}oDȂȱd¤ OjP"ɘE B,%FPTh(,ARP(2Y)UH ;/WGeB usnegoBl"H@  )zM}Z$!yG\C/S ^Ku92ns.۫+$oJjmeqfJy8m)8[hӫwo?kJ98dK&  s!Q lEX.@*MXinF*@, ]EAr4QY%-Ȳ @]HA@ au[itxQP2},ԚZUUivQS]zqlzv*yɝYnB5jumUNRLnMY19wJ|0;L4X=F[u~ 99!"q5fj}gpfqmzZwy'RZPiRB!@ Qj1U*D$@Z!UJRdkB6@P II-X"bH(("0QjXV0 @ Qw-:O[׫Xj}ߠW5VjRVUjpj:|oXoc2wcW}u\_o ]\nK(vLbsr6XF3L=QWkk=GǭUd81q7=.N89Roɸ&l!5 EF!@j -  `EBա]XAE M @ ( 2]@bڨ6Qhh.4 (,(5AlA4 d3EQw :v:W et><4Uo9p]m+ CD B!AuBYJ Uք UzH-!! ̺VP (IEZ5V]EиXu 1 QI*PRR d Z[E#*)]7_Sar%T~񳷕.5SMϜ)u8|S%Nɭ[k' }Fg' XhqSR<-_ŁmR]W>3Ɍl{ż^I?_Y<|y=k}?wqjh/guD~_w׹fVǘ=NW<5,BEUր"BAm5@)@V X %HREiQmZ. T Z [ q4RYp!PX!iu A`\Wk{jAMR>nmMdb%:=;Y)Y Vi·2*jfaVg%NsVU}Wz?9wΖWcMrq~~30G~wz.k>z^~dcv:r`u{ܾn _Wuc볹._Rm *!MB4 A@ RPWZWVX-]E )45@@]ұ)Pj2)P @% Ir* " 5E `d$2AJJMeE ռWbW_hSiKՒ\9;Cyڮe]19\*7y.߫^"ժk8x3^?&!~FFV峛35ek;O2gѱ6|Kד?|gx!=迤8^lyo/[˸c^/욘~_/eoGzu7=!A- E@ h д !4H U& .IK]h T $&J Ed"@, Č"Ҳ@ :nZN|7Wy]jƞS"P껑[zn;輿o7UkukM.f0g"֪WV֫}oxI?I{?7t=-oͯwp:]frn~K5ߤ'1z>׽aL~wòLu=ޮ6.s/-MBYhhUP@V*ؤ R t*(S!5 $*VEQEB RahCET*`+*JrʒHRh-Zu/'>fPUҳUmMr(ZWQlvFnMfpy\1jq9y0q3 Y\XZ[Z]OܴWl~gpq f]fpqq`wWUlh&Z 0ل+H2AUHEAH@, nu`PU֔2 AP4 ,Uغ-(4P -+2!(k-YP\[* Z--M`]E2ǁs}DU\E+JMVj2x,N^T֓Saly7ɪwqN\:[jֱ6S#7VؗmnCWȺ9_ Nߙ}x7}]>s +d6a 1B]X-$(օ(hPBMXAIAQU@@,HB.-- U(X5(KZ@- Pk" $-H( P`xFq9xulnU*Rz!^UfWe1t\Ww̚,̣u[v3FYQ5+n&~oVfxޥnk$l/3ə.wNù>stgwa3~γ=ِ42فBC?{m{EZ-aim@54lD[`MjJJUJRA@mY%((P(R)Qu "(AICX T++]eEIJ`EY hʀȡE!wǯsuYZA4EAR'6j6^, XܚMU'cr 9bscak?ey}'WNOx~w濡%=n O㽿W;.z}ݎAoo}w7g翢xO޷26Gם^uY>O_y_Uj4Ԩ!PUh(EZ)H 0 P5T[tQtE"(,!Vbr",T]*@` A P ȰPAAAB b#Z~ *4ڴB,qͳ7u;6X\mW#*-?&Viiut_oy?kTɎ[_w/^M_#>ps'~#a^<{/g+#vߦ|N6ߛqk/OG|NeAthAH- *UU Kt4-! , @( ]E W@ 4R EA XEd ["X](( F(2YZV)5?I| &Zl[nunVۅҶo6'.̧֬ۧlىI0=#?JOOs`eUˌLh8{~nau:w<>~OMн'9ɳ{'wOIoos1<hAUuiB֡J) T"R5((j& )J-(4ZH-"2ʤ.K]j V)nEnV*+ ҳhYk!E25)>/VTi۱6,Vh{w3?!^{:ܜN|y{\:躝k<K\wҿx ~ںo.'.Cɹ^MG|֫ާm>Oӿ+}#]撾IO3:qg{>[U^Ͼkۇhfw:wzK<_I ,R2A4UP] "ĀAiEh@(Q--(k P A+*. @TV@d$EQr2,V&+迓Q("ƛ [snYY >!u1}]W+GgHB~syܼwg]OɝgY͌u|ݏ] ~}G/t'~~t܌=v>;)G~v<*}yξs;{G]~ozX{.?& /|k8ݍ'/^WXP&E- ()HP Q@FFSH ( Qhh.!u  d@\31tQl Q`PeiYP24 Ed27r4l芭 j_:|ƭŷ&,_e]/|5|v)orck|.zoR|͟Xz|_gqzؼg}s6|}Ϟ^|{N;_U}WA5v841u>q;}9=YG_?wQIK}/atҸo;3Ãsn~c9= q:Z)W_U7 Pl BIl$KA!PXD$X JHB B,@@X @*$&w-R A+#R\"hR3ɵb r42ilάmNkFw+'ŇU w}LVN;xZau>_]z-/ 3u;o;Éɋ89mxw}67׭%Kw=?mhrϟrow:8ɘCo/kE;yOs]0=$Ak96_5pYcNЛno'SKkS3K4^W}=f~_C 瑸}uu591އMlK;^_;4= bgC^-!;5-gKW[v~f'Kzx{|}!,< 0VL+%Ck!0*s0kYO51ER"d ̱uk_UdE,@󧕳lF|w5U7/;|OV%p5}|t].}`xvK][g~#1n#7ϟگ<v;KԹ89ó8_7${c+7196<}LOm|߻:|͜isAm_ss|L}/羓q7]18{~I:ݞ;Lωw~sϽ_{]es&S>+r/;I5?z w.3w|eB{W?5:.N.:'rv4֪GcN7eI;wu>)ɸ'r:I7qŹ5מрŖXD[+r4 _KTZP@9+M6G;k['~cwo侺N^[=7N^Smpv.|}.0ywyN}cKCtOzV⾳y&Û~-7Sgӓ6ݞvzu=,nLEhu:xya9zSgŞf`q9oܽ>pfqsz}u7cX~y8n~\;Bύy߻_'sS_}۹6Ӌz=S76_;7b^s7ɳOJa⼜.Zc̷fdžΓϙ5I@)h15ʑ4   XlG6K&u_/7u\;ϟV{9_qz?oq>9xzݾmy=.ovz)9rpx]~t{&RAW͗z?z}GG} S+\w#|3~{! yxxi}Lq~nC.\f=.N:xWv>_!~w?Ѷ]-YiRn=o9en<|ӎ/#'c99HIZÞ˾3}'] TAr, ! }1>"%B$Y 23b5s/xߦ n߁켏vS{K45Tv5Sɜ ;}qfqH޷η>[Bk'ܻ}{՝yPf[8HJ&z>o].W6n92%"E7>G\k>~67xS\>ўZg6ޏ'qsZ^sw!bv>^<ox>w_937ʵҼZrN?gk{އ%d*;cw1kKoqWot}-&FeArG%(@AQHRIlFƿNL2tys{}i@BDsBSaqxynLmMRh ]sUFGh_ߜ<С qRqٛ !B,@ P$J!:?\^fN!JJ@ Y J"B"EBTD d@YD!μ~  \D,Ϭ_ok\ ain7][;oJ܍go}Y[NwquEM%<8%|Yqq7VNW;O'/+,7(ԍ*+ֱynǯձ_||y|8ܻ>Sb䳛ÎkvӽYxO+gWUEՁ!P!(ZJҨI$!VY"KTT Rt4J[- P BrlT  Q`TPȥXl  E(X]\:O^"}'񿠫TEєTҋv/? }eqs6t7ulM׻2&g.-2WGQ5/[=7>wmͭ~M]u._g|oW-{mcoxm{Wz}w{<<]LI?C.Mrx=zVu\?'[! a((!ihUB]hPH A(,HEE**-)e*hRҀV@5, Kc"A)P B, uQ@ʊUkڞSl+^xWceʬko`ossdc[o;BެGlrhuׄv:7އd_|/{o'w޿ ^]-/A'/}v_[_i|o7Gr:gˎw>W}]<y=1{w׽#>7PB! - *)EE[WTD[ Z bHT B@! `( @-(`X]")* ȰPIJ "PPXD.DA(\rp=L~w+4V.X*q67kZN.krg#տzf(֨f n7O;~gNϊ}fK徻ϐ+?L|S<v3Ƕή>=K^_VqL&,:fn|}ʼQzJt]s\>n/aV!J  (PU%HV)IAeT%T[E`-(PKd"AArKFTQl*AHddX"Աh"蠹VhƷqȯCu&f]k|??_W}e#2oFf28VO&\M8bsrѵݏ_'tzKÃ%ZUlm+fۅQ A %Ԩ(@U46ZAmmT*K(YJU%( ZXPKȠ` Hd-CY) EȥAb[$5bDT(RҠR ׬su}/Zį~/UVmU[2piM_7Nw}O#;J5U+֪MZSZ97]TjfpaMji4zewOg۩_*]UZQ-]v+eCti!(Z(.)." tP( R%@Z---ZZJ[E@JEȰR܀ A`nb eil [DQl% X`PX 9ZqO>cbULӫ^[kLm8hUyKvSZZu]ZK~KV'ZWg4oLūTV6vO+q%_+1;\rթVj- inij:|0aGy!%QhUXԴ(j@4 TZt$PjBԱAKBиr0)HKYKACQYZ@QlY$,RXjAY)`/ k?P'ԮlSEJ*ʵ*`l{שN0up&^] Z<\N}6"鏽̜L<[}3wc;/1-ŵԫJ*4[Z6ܟ+2ajFH2AU@AeABZRiT$YB)mPjT[FU @%-AJ-mQtPD.U VB܋ -EԈ**K.@.EE.bXj()H@ȡAs>_%Uz;WU5غjjT_ǓZfs8r mM/zuVܺux%l fs&aZffr~nγ]*ux/:ϑ}:ˇʕfr={淖ωzK+AB!WgC y;|N|" -, SBRaZ%%EY@Z%]B !K@E--.U(Al+B s,BalW@بPDEPr(PYff -Y [TM-V&%Wt(Xs^-r8aMٷso[k)~=Q˥쾳;|/?4ޗ=6o_޷=o?#+S W` 0=O̿7]>goٝn>WGbK@)mHAEhjB)m *P-T]@l @AhĠ@P@]EAKJ (X(ŠȰ @ V@ E2DD,MHʃEDf `PXPdjZ,Y%[`0)sp}+ *Z%A5kըIV+smͻY4Wy4\./.G'o_?hvw~s.wz~{gydHE(m@WV !T )H[VЊ iPZʐ%H)@--- @"@ 2(02lV*++BPfР(J\2PX{IHI*^*Xilܭͻ6,Ͳ[rl}mGwx]/g\u2wrx;WjG>cS~7{'ɿJ>]]g]?'ϭ>|f'pߛ=~NG_N= 8rv;n#½'Ͼ}<\a~G-1"T! >ZAm]YK"в@[EAEl!jPM@PhACE Vrh )*\Ab"ܭU QE@аX")E"Ve* Ir2, ]gX/jAAZATZaq5Kt|?a<>8o_{ϭW;sߡ;C?j7o6=~j|l?`Ǘv9m/]f}?U;]T'ss^om{,cs}Mv]o??*}ll:އ'~w{/̿W> Nn=y|? dY-!(]@P UPA(,H-DB--,Ũ*KA`*@Xj]J-XB@"QH* UAFb-bUE^wzÓ(ҁQA.6p:&˿l7OɌ]u!v;~>{y~;KK_q!am:~/&?Ey?sr>~]Oߛ}Nߠt}Jn_Fmgy;vz!1x=o/>O'~g~ǞzkVwO^Gm9}?IgŶ'/6=xu=h (% hEZ d Z@Z-K"*[ J) @ (,[A\@-Ƞ  PJ#P2Xt~zߔ+~8EPV-+C|>k;7>k/?.>o;~}fWƿ?= ?\{=>|ߡSŸ^o|]S~a]ο-ut;ؾ{/'et=LzÍn}[e?湭|W#_}ϰz?fn}7WS-}uxyA h! RH%-B %Z* B-BP@"K !)R ,؋Q$ A@ ,-QR̠AQ hSQſS֭>kjmh5ZWZi8;p=Σ빸7Ϗ@ߥ:YŜ>oGt{᜜7|x>O#t>Cżc^?_m_G#OY?88?a;ݞO;Yx򺽎/Gŷg}>.Sݷۆuy^~'ד=}:m^gQm84|Oڄ3 !Qw_;C=i;K|W׆k9<ͯO8u].B -ۛ_Y<6) vs: v<z.>I$'W3],n{0|LMqyo+<RF˝ 跬n%X- [B 2@$E]@"Țo'qr}%~YO/ֶSoO?}>0C}}^{>K]>Ft;qyc>YVys#1M~mk͞k;^)x/v|۷˟?Og}n͟cvpyx|xgG<˫V0}^Oɱ/4KXt|?1N{]!8/_~Kz~Oq'zcξwn.KwM< {v yޠ5޷M9şU}wE O sߞ~Wߖ>0;>7j= YeW]}6=O ܫ松֢ryqݏ'kݟwPD9]Oca%.^qt|ۓ4zgǩzoaiz= ,uz)eQ/gP?;Co7b"xFh=;\:}ŭO }+:K2?#y}vCc$&=3zC~KA_[%ko,yO?5|^IsiOϹ8>{mvzuFgx_d'kpܸ-~Beg[gkuy/}ouׅ_}~9{~n+vCVh_c}zoXCs4kZs|H{V|{O}J~~Ceg{r=侏3q\xIxLm{ :_d~mra7u=7ѓÌsewo;Kwky_?V8쐖3Y{|/uC\%[giv^}O/eyt9<6YWJoAO_wx/c^^+5Ǭt}~wO_-ͷ=N}xCӾ?=S>yczVv}tϓ~'%^'s^ |W/@D )Njz=/WO3|syG'C_Rw\}{RNrܜ|>IkUwC~?aq7~bAS0^ᮮ:ȯ@ytVk3=x>q0:1^t]}ggi/L R*ӑދ7N_=z;HMxt)Iz]]av8/vѬ}rW'Z/O_;/cox91~̲19.Cv(v;Op/^~99OorJa56}}'8rc܄6?@F;X}O/'o'cury8=g/>g߫^-?uy9~璏 ?%lo:q1u=%- Y~Y/?}o^Vҿm|DŽ $|nZn 'Cx,^}'Fƚ}߱slחy{2EW7Npt7C[?_|+uߠ^.|_Ǿp'.]k=n壟6n9e3 XrK5'qr}/>1q8;>/>L<.:=k#|OBT+uy‚gOTsJY%6].ҷ:Jxy!%c\"]O'=|tu*s"/-:КkS689؃U6W~K]jz=݋ t;^yky??v}{7T֛,5GVv7w[͝nn6'Nv:t P-UAFuܽ<}aT^{宼"WCz/O2Jozި!պw5V9K6.NunX G'Gwls Yrr][¹EíTί/tћӇo\NJk_C珛4W_}c@)0H ,08;>vߥȋl[u'fDlH "xvyi -ZռqV˳Ë\MS։-BJR(HE+]򟋷 هÏ6}?e y/[4 ր[4e.RZHE, $ )hH, I%$@0BX$ piOM{RxiĄXD1Qd DWő- ,4t}hb)fgOȔ"T nw׍!@TDH !E *CW|uMBn=ߟWT5}nPE (P@H@T"Y % YX AA - dB HR@ %D,PElm勺Y[|bQ3rieLNl`[M 8Mk'[)^Z2Uni㊸|nj~JF,~^FjdQ*֢]eqfd\Y9rNiXh@$*! @,B(-,% M(RХBDiP R)( A-R2@[(@Xd!l, diP P UwܜX|5k ߚ37&**Ը^% wAp;<{n&_.{%uxӽcYVMYǜ>mٜn7.75ZkIVeͬ,߉O#7-;wkx|ܺuz9bs^G3?; `յDց!RB]XBd 5V "m%QHB @-րX(UT PPR袀.F*+0TA(PR2(2(B. b@6`su/*WO?%TU2Z50:~~c}KVN-SzteZ/6y3-U&\{ފ[ɫ"& )y޻19z}_]Sx]Nm[[6fo;z򸵝|ԺqB097#.)H XBRcAE IUZH$ 5fC@ .օm@T BP EZnEPPE`ԱQI`**Al(2"1[EeE198u]V ;@jZ椬:t__ʱ7=>ǧ2eqc/ZƮ25̿ CHܗ#QɗǍɪW/NKW]vʸ}g?S繵tq~o\߫fo+R|kQz=Mg}~ӫ<xy}F'>7Z7+uؗoC>O}?t1ɁĆo-co7:Ѷ}j7.΃ruiT!BhJ J m(.@,& -I % Z(JPJ \ (jKPY(K!% TZRk*(f_:U3FjJ]-v-yW[s/_VQv^:UKYǛq6 c22-ٙVui^噶[zy\|{nTG?"W:&0]ٷaft*pβx` h BP"aP 4-.(+BJP )VP T&.TQt[QR"24D[*FT,]- 32PZJE.RWkj}9e][uyLBDao빸k| Uխ4ݜZZwlzve^ZZd[wRK>ŹCcov^v/kgMZ*ҭH2-ŷ:|iB y!!4[ABeՠV 5A XbnH%ШLPԀ@R h,UZZ]ZE -IR +CQPXh( #*K"JPPeI"ط5k_v:CSATRkJVյ6ȣ\tk5i]Ek3t}u95ËsiepJ[ɪ|dq^_GkKp9]vsK288xkxNI^h V̟:+2iJ#d0P$ * BV-U" * l jVUP.P(!AP[&[ Rʒu(BAZT ".+JAY1 ( "@rk9zf*ډ^uMUZ\lɫfɇϭ[٘\JŸZbzx姒cg8x3;]_7C~?NNf7K{Ki2l5 Bd$T4Z`)inV`UP i@.]@,C @P-ҋJEb(\2(("u-ȥ2"ܠY̥[A*y<̮)őuvsŢߏ?o{:=^7ow6ní_p?^iWɡȜ=W|'D_ץݿc#Wcwc~y?sf-C5Z7'~9zϾtÛ)Z( @jiUl Z4 ڲJPPiihP QiRR DQX-(5VWI2ʊ.A`-C+Q.C4_;?FKK*i])^.6Oavm69uYm3jYA.4fO܏_ /X<;_1u_g_2u>_K~k oz畏׏{.+~ut{[_N3>Uj4Ԩ!PUh(EZ)H 0 P5T[tQtE"(,!Vbr",T]*@` A P ȰPAAAB b#Z~ *4ڴB,qͳ7u;6X\mW#*-?&Vy!O<Ӈup0 mͯ/70ۋ}6s?_1>+_z4o[_U_s<Yvߪ|gs wߝoIA\S埙}͝niAV (hi-EU] J@*RQtQhPHAilP et,P2S@ T++rʁZZTV@ ldk"%b_ kH% >VŸVnm]+`M ]brjm|ly6[X^_n^ϗw_{}&'gy?_vGp=M|on׃/{?WG7wz{~7{'o8$:o~ot4Kc_;{>8_#}os3/- -hB F!)H@P.  X H@(H bb Z%X02C*- -,tPIbYC*.g 4b%#KA`Y-fݕYdD)cO ;^r8pֿ6:_ukwk-5w:nY[VϾay?wO9b~was}D/es~Wuߣn+S,)|__+_cz*Wx-+FcJFBP )IրAE V (4iH QiA.@(h U )tRX,SPRKr( rQYV#@(ȠY.b-ANWz " KL6Ofݍ]bF}WQy]mzI*>Or|iz>/>k: c}^Sԓ|o6~f./egA ,R2A4UP] "ĀAiEh@(Q--(k P A+*. @TV@d$EQr2,V&+迓Q("ƛ [snYY >1~zt\8W<7r\/: }\3=d[._T=(nSK+[a?c?ak)䳋?'ܯK{^7S++s]gy7p]7?><׾vs8?//}ge{ܜN_ݛBTD4Z#K)@-EM ,)E"JЅ(2 5Ar( E2,]E@€TVFfXE@ ",Y!"ozղW*|jܚ#4Hk>oKjgoQ|rpvɿQ>7&Rs]_󻚮_ s=w.q/w+59{|~ :o׿^M_A~O4>&>8`>SUgÿ?}kxl=W~}^oeq=+z]}?]w,~{S4<wx$ Fu)~[*uvJ3P/M]&vyl985)uʢp:ݺX9]E A"@йl`h(U ,zWr"*JB44ًnuf-ޏCaq{2>o>᰹`gF0xzo,A6cs1GvOCm:^Ņs7ݯwɫ:!j^;_3n}+3|Z~kӿ9rޗso=׻~nOksp[!$?o⭍72x}xq=C7/itS뽿賿#񾇛R]|7(ixfg:;ӳ>_=%~U]C~/s9*.ޯ/D~GQMx~<ض̶|J'ǿ '{?|V5Meoo?u岽?7{wm~~}ޯx_A*sg?꾧vpo+cK q\}oiCn=yy|q^g{ؙ/w{Ag|ڊ:g:y;|?;ޫy'/_]`}YYyy>N7][>O0Rӕ??xV!ė8?r}Nyx?Rm7MV'zGF=/<SG"h2k `fXz Bdֻ;ߝ[/u_Y}Cg~wxrq,k5;zs?. /DNwk=xVqŽ.r'zI,o}'zݎu'q:]-788OGӾ=(u/y>_N)ug>Qç=#.ŸCG89F:s}''erz'>rJw|[< 6|KMjx/KYe@E2#@ʵ eE ҴG Zͻ}^~Sc{o0JpvC-z`89+]oݞ՝L'>ggo>{]~ :P91vz鸺#9Lm= _u9j8y ^6@r$^ו럥*7gyprLO~19/}~[v<OC1GX~O_>:_U˾'3`"P$+ hMr{y̎㭞.wRl3-l3S(760ӛ`οll;~r{O>t~0~=/TOa,>_5W x=_ŝ5Ł==$Þgo/w'_y`ox]?GRk|y1K@ KWRxwGv[2/ü:}%ܜ~?Qox7ܼ>_<DZ|~|ϭ>Wº^>ϙTAr, ! }1>"%B$Y Lڹx+Dfy ,$B5`,6YԚ9ѩ%h2B ҴY1U[敕ōwcuom=zdmƲ"ʓK,$MJ[/|u'aEtZ3;l?&jͣ?u2sb \WQ9Yy?G ]4k={=w97٭Wccڊ0mcekzʦC`z>>6oo;n|tUE" kp:~'o;X^ߟ\c{=.{% aj3P\@4{J$TR$REK!Z` *_k[U"TO?ge[q-m㹱9y-hkǹ٘$"B- ( (ZZߑB}|ʺޞ@"R )TR ZQihj(@JR%dRȶ@XbضAJ ,TTDAdE.T1"&eRX3yR I>opS!%Ia(Y ԅhPP P9[6, A`(,HBHQ7v5}L$!K3@Z B BP!JЂZ[ťX!,R ,AA(Kb(R,[-b- P-A(,@3sY "?9 !"1#$024@`3AP567B%p'&Jw:%ldr0_!~`5݄eJ02I;KX%Bp %x° 2_`QdZGuOPn+%$6]ř`)y/r7 Kmpl&쑗`B{D͵>M,pwu{!;E.PY*d`ӵ82xԨׅ tS9CYgqA /Wa H `c$?0"sJ%O{Y/b>ӻFx#A=,Q)."H?p dh 3&Cɕ$C5or} <֣++̋bw ZMkJQeKXŒ#<]Y6 qcbG8NwVu$nawIyԬ8j66e ZÒ !0)XxciNBL+piRJQ1C,5\e!F?`~NMO_0 n$qa{ʺhvvu'eS'\Og6[2 R0rpz"_qז_$3b`NT dznZ*io *Tӧ%6&-l+J[L3Z/bMSz3U!XijmENr;hՑd-^gd@5f,}R6= V{+'&t&`$! a%Î-DHյ1̓<}Hjo霙!)H/0"3۰!DP<51l?gU.Uh9efI~Ho5VSdSvA#59i? 6xPW3Q;N܅o'W_P7Μ1bTTxjƍ Ti_~&IZ &[Mj.?Ȍ8A<77MA 7?%,tXWvT 'RRF>dEZYNH d= |eN/E]qҿ;@džY%moz?4#Ű⾟٦&]m݆qLcFƏ؋e.ON}5z_8| j:\M)~'k{~L+(k",u3]S_ό,AGeJ0=-8mI|T>R)ą}sڥx1qf7HI!`d ue 325NODЩqd5txZz cnAznԱ:V\>K%,Hӵ=-V``Hr#NHr KG"NӇS*M4 8MǧГ]A /Ff=d{$wv + r4H^2?ty&h܃ F #F6Gw`J Q6HK{0 BHI{1Լ:. Lu ]s?* B:`W5,’ `dH`%&R fJKa~-;k $fO/q_E{+}Ce (I 3NR 5G)KJ/V?0`FHN JQ$+;ag/p=xТnR  {_?("FR 8 %ġ:cb+x z@ǒY2nuF=, /> ````HaH6~C A,2am?l?pm(e92?`C5!ɍ$|ʶjMl.\菪},H܉YB8@L"D6Y=IzcVJבOW,0}p00$Fܑg{6_&H9}!.O%(b"DBIPYS͇"rPӔ vOK !᳴C&e X)`(N"5etSRj"3HϽu.q,}O~͟(_}$H"AA2,YHro& ,00000$ D"cRQ~FW{?6NO#!}o9ߛ mre2 2ݚNO:µ\I#hE=I4Z㢰LȑE>ˆWPG8K'I%_W[+ע =bLCmrA8N(V0~. D"Joh72[=cKIԦܒ(-˃VOK,[ƑhuFa_9":Њ#-eVS&XJMGW^"VXT6= Z e菥oM}E^ԗFoOM)e`\dYW;T?5mٻTwJFTتtM$)>6; ӻLec Dcӟ 0@lϲL䑹,e6SyA>Fkse 6-d@ŗ\T䴙JrRuNSzSK1E$«^ۓ,pms4!s5aJI*Q'g,+q>((4nfLN-|?KS 61g4ne?͌ct"@zQQ"Ə6Y*JPSO$NO Z R]{=uQIB6nXDSҔ5EnQfKqw `m5Z> 'R}]@qbt{L4AeKQ,czֺUoʷ^)Ph!~z0ѾDГPd.[fOg~ ;]Tl5 0섐vNH^lsSDos# NTm|Yx߇6k\#:mXA[ \JCiGZv2I}rӗo,VޙhZj'B[H#(9֋gIFuC8ΐ[l & īFgc[SgLMMWL?uL A %%K 4Ƥ;[\0 a͸# HO{ )6[=/z{XWNC*,w7LV6/#in/a, ųuJMz+_Tk4 $uۓ\,MT.|ڴCHNJڞop[}=4vH{ud/ R]VݜsāB5Rw2뷥R=V i}ƽ%(欁\'%5 5Z-xMb8IVKOa)80m%8[&!θ$KVPmZDFHHrSMuG-> Yַz3r/Cj f˽RZBFOCMɧ\ t D pH,D-I'VĶa]`EKThCQ8̔ҧ}6M&b'y(֯F2IAeM{I i%?Y޴'܂9Z`sMӸjoX.>]6NF\Q(hoqMFn_)7s>3ik(ʾAnR1 DO(p}#;Yq&+:CY'+_G7ԟE9X_S2&iTC]vpVzze+gR-ug^oiZrc^MY;3C"U+?Z+Okk6&|MpMOOҮiԬDj;-PAPFg=44hWI)oSL !D7 .z3=,wqSUYKM%ː=m0^B܁-M&}¸n@D$$BhKuㇰLuSdm-lД)>Vf %}$H_0$%Et]syǤc-|-cHVMY0%v2协aI&_f>e塹h LbzH/[!hX'1tۀt[& Wh*lv[alvƛ"CRHP +6ɋadh 7DolT5Ei{)/SOE8mbC8m?Eņ8-`BB@2?(}K'*.{`Rb"m2vo-9g-hycC":Y~XM{:&3Tƕ7Nvڲl}I.Hg9$dɩӬ3[w-yuq|ECaß" WIJ}㙴I|qhQGy aq t YQ\N~8&aOgp%IYH%刣v }:d(1&ÎѤ7)HK re;h͉6NTGAt.B#_ܽIԬ$ҕZ"(ZZ֞5,}+t="]%!Ţ*4;z:ǗW [~Lof~tq!EL};M5~M&THֵQ h&cn=DAkIq#`5rV뒴2CzFF34A-`4x t>B$A6Pd mY#Krh̫>J{k@앸5Q(d=$&Y6Dhcun{6q-O LK}剩q$IZ ռ!}b'wm&rl4>?*(fen"GyBzr _KX)h5nGnYqFG\n(e֍9v75VÌQ)[KJ9fK_d;JBCiX Gvѧ˵چ%>F#Vz5|uzLw {CI뿢]%cRv_Z+n89GlVL q2`v]Y'kp+&eEfNqХUcRZiFr7 Lv\y*TO3<]iuuuʻ'*&^?zKQ*8[<.[_I<k7x%[i al{oޢ4J{ C=V4 ISe C gW-mZM@pI>K-;-eP}L{:$!!Jگ^p#}RDLAsJRM%xB dgu< WSG!(ӹA.%-%oa*Re-qֶ !mX4PYL_9x"l|FcFm6>%l|J+CVĭ6>&l|N;C 4>)h|RKC⦇M>,h|XkCև>/h|^C-Z>1h|bɡC6m4>4h|hѡCVƬ2>6d|lݑ#vG ~FFzdddy\Iy6la3U+kM+ 8Ug[`yڝx9@yBfG4Iw;2+/no/`[# 4:Nhk#H#3}F5`|#I>?VSԾ.\Qo5-?{yx88#8C8C8#8#8#8#88Ǘ  Ǘ/^/R2 !.`gWANn0=/el)e̡e%m2 ؔU&KsِG5qHnu GL'TH VM(Cl2t[pMMi&i?!ͱcZYH+n595 )CcbI3 ڀ2pA-fHgC/H,B4ݴI?CXz&šk<8UM@'Vi5,S狼Ws5J(Nh:&uEd|t+,Kڱb;T@kh'V^0u}{=;* 4W<5| ( 4xHޥ*ْjZɂ#^b.n:ɐH, f]1jI M  HR^4 @d}e'>u#l*v?`pk GJLI"Qd%yw3o]-k]mR*&YSQxAe7?G}%f6kχ5j+`x|]cˁG]xIڛO$l4K_4e^-8͇ZK3DG&זVW0㵣`wFIi;Oo ;6ߵ5|϶edhZHҷGI}Gg2Xϲ'r6[]Y|/@\&Shహf6}ѷJa.3C`؋ &{8MMti/A!TBˮ=f {W2v6NR"em{l}BQ|A{R큢j?D$S^ǻp% {>悔S Yq!_} $.lpj0^_ CE#1+qs^, BOh7~6,%iAyMtw%D@6H$/v'lHLd{sHJ 5e<r% 䱐zNܤ6QaD6Z;]ZB$T 2@LŤ7eDH&}ǒO~JfG-7$HdIY( !) HqڈZH-qFP<}zQr\J((zsQУr[d5(RMT,}5112ML9/Q06:-kbqm yÄ_A$A$)q=P2iQ8)pa$XwqDc&m0e+2 7ȖYYAV{$&&k(ע<FwlwQq[Q(NQ*rUAԇ2"Z{Rגc__ЊH_}AG64]Ti[<_مY~&"@ ߲vC6?:p rmq]HF R'iKŹ} 0$!(k*b!*m{O,N]R4T.7m!- eQn=)EAYض"j6]q+%M&}r?/"@ DLtn!V$Fo~Sq}B^v6mfqv[RZx%k`gX~Cg,LLMl*[skԝ-nQ}#;]-P.Y)Og{da_j[x -L8~d/z? , D001yy BhAJRrf(${Zҕ9,qFEqƢ. h֢ZJ# qRPpJ!̖ƥ4MIKqW:y G k=`۩d QEue>⮌q<`Eu =p%Ŏ"mXq5M*ˏ%ݢoW/NZ*\E<%VOìzY5HfAZeru~. ]1Piq#6pV_bn.JPP5Ŷ@Fa/j klք%L#Ҝ蓢yB s"ϳ!q*r]n<6weְrwILv<$) NĹlw`⺙ę+M:EM;:ZV6:ϙ HRuHKh2H"yҤ=Oxx||w r 8|%)9 x*BA%I[祜ؾLkfæN4mKvQQ|8V4Q'Fek aDcΚOŹJNC oƧs ;АsM@$TӊVi]ԪJ'w:rFYw-WhmEOU"ThM⤤F6#iLyy!)0e3f8qqaaIoOՎ kOsh[3'Um (0z;$/ȃoԥTne~{X%q$\8ۖ4앻bټ"ޓVM{򥾹'װئ>T'":䱣AU"|3ǟ_cjZ RK%hf!'*ArVci[pΌI֖a -|pd7(+ y DFz;-~qc>dL}ަc``cߗ"L;-2>-i{QcW3!oBAG]]hmjk"1|ndGٙVa(gr = !N4}Ȅ& %6IS卙Wq5è\ׄmrq})56"qURSH9n"b1 %']!Mٯ8wD-gd+J$Iv%Svq{L׶QKN\˺bJ1],B)혅$ZfKפ 29 W Mp>hȉie-xMbXY-><زK,&7bgX.6ZՇO7*ڴ_MʉpqġmSwYMI"_H'6zr#-9d2ez=Jmz)GZm;*:ٴTԪhIͺCHW$Fp00000tGJs]5ZeM¶ 0X?ϛ!ݒ8ݑГ EqC'ɦJn SQ,HICiARd0d JM}$Z[vf 1258:I D}~SzAm_w<ݫmŚ}8jWF.UL'#.T (4GTX8{/45Qke_ I^EDDJq7)qɅ"LҊŬ/A5_b\Iμ(JzcQuzdvr$Ye- C.zKM+==23PxRNɍy)d잔R8Tj%Xn2JYr0|*k҃K7oljRh Mk,9%eu]L?0U3T Qt|uGRҕjq$21N)JBQǯLvlX< W|j k$_.-U$EYqKH9idmH]t̾2S dgb_K.(n酱nһpN4}:-IbU/.ʥ!O1Oy;,QtU*(B)LRU޴waztr|-k^^?G=rٙ1l,Uto|O5姽GW<:P}OEMҬ D+YBVOqJ a=_OfRyFn4u.Im j,=/EHu4ú-SZ9ZGa?D.C̻sNKDo^nxț\y &&js_9OtOthjTWse,t[}QS&Oݫ%[WOd]tխj{h~_\>If 3Z%n+O_anlnleoRJ[h ÊZ28h6h%[3nm)q^!Z-IMT%X߁ YEDbMB=G 8GGje%^ b[|BG$kDX.c6IV96bK3\U2 #=o_KX)h5nGnYqFG\~(e֍9v75VÌQ)[KJ9fK_d;JBCiX GvѧT˕چ%>F#Vz5|u+zLw { $b9'G]zJcxԕHEmg1Vn۩;&["RXJJuHu\t__O*NEu#vR;d 0L$'o3#.BС~l=p$pwT; M:sdZ$}MaR dՔ;:Y`۽oUk>tëҪaurhI *윩ux>UaGҷ'ĉ DoN|Fߋϣ>L)ސY$>Sv d9;hɠm2 !6fA/ pY鑑!Oq22222223=3####? ϣ#?ϧ?oopoN@r9jZ-CPr9b1CP(s9j5CP(s9b1CX90sl*JN%XsV9Ŏj5CXsV9Վj1cXsV9Վr5cX,s9Վj5cX,s9r9c89r9cps=ps`<.2ty̝d7G<Ϗ7|ynyo yāHq ý2s ý2u yԑRGIu$yԑRGw$yܑrGw$yܑrGIy(y"(Jw:%ldr0_!~`5݄eJ02I;KX%Bp %x° 2_`QdZGuOPn+%$6]ř`)y/r7 Kmpl&쑗`B{D͵>M,pwu{!;E.PY*d`ӵ82xԨׅ tS9CYgqA /Wa H `c$?0"sJ%O{Y/b>ӻFx#A=,Q)."H?p dh 3&Cɥ%i(V kQAJd[`m m?$J[5(ȁHK |+'MlԬ8j66+?F {+O,m iZ%n0ҍ*CJ7f"eq˛# (ԒܿɩB Đ2?|LH8:sR\էDjJ ITivA t6vWw6,m4l'/m %M;"Փft{G 5VF{iR[kJ,iq,gFrD:[ L%H CK!IZi )) '($jcy 93FBS_Pag>Dg`Bi:#Q<^Ok|h5ɴז>cZsH=mM7EfW-V25mKzJQtJViDJ酾V,u֢֚ĩ=;SSjj1;tVŨ(t ᾚNWv30`SH"e\)$3khӳ^Ժzn+SiI7*s"0ch"XS@5 4Ҕvܔaa]RII\%9ifA9##C,/e~ߘ(6HJ=HVՔ z|)G^ Էr)zSTAg!wAm[65|܇߄Q{oȇ :߇?zvDrC^xHsc&\},?ɵDmgA{xa^i)Z I!-9-2#  R?{C=VPȒ ~?pe 6JB[ؠdg(dԺBBK)!ֶu0`cg<Q]qF $@)fm&C C)5M2R[  >Aiޘ;Y_Pe#4}{+)^PC/t`mGpHQvJhWS_3A `006{}HBI;WU$~PDpImDH aN5 pD 0$( 1D #a#De)򝆱+þqԌ7e+菪},NuYR#?b $&) +F5 hUcG=%ܟa } \  I$a(=; 8geH[Xi'#VL' r%*hl6cOL+ btJ+t&Gw!N,SLDvP&ǜrM2ֱU?LY`````H"@D)nڤp~l xfv8Wȟ*Q5D}{cMU[-$k OZ(ٕ%.ScלΗ}:v,.9 {"Qɫ֍Zv%+7EJ o-I/o%4Q Q)17pA"uK)Y j+j,0j7IQd~d/2ʗq1lR# .qK-k6.,ʂpc#.O 2$*C2}ާr{sNZg Kebpl,)[AvjiЪ.ܺUYɌםj)$g@ /Ds%F>佤JoY>ڝ)eL6dMWT鱢:fja<.brFVtǹESEvѻiͰb4Gv ̊>.fzAr3."!&nUxS7V3c/zH!5q/Y!]ol/^a\g( Cڌ?\Ts|#Fg#ԫ$TtmJ55"K4pJUT)2a+tln9E(A{q{ʨ2(#:sl068oHVlm8SZAR-rg+?%)L;rzn_6֑qe*͈)5-*HA2 K0^_c1]$ci6c9"m&Ntü%[cO8Mkv.VHq}M~Rl6gQF1P’ʈ) g; 2“*mܓ+)NGmp򑀟cW-΀]Q֘aԞޖ$&Gs/MlX"wOptȁ$]޶m1e 0i {D[+b  6(lPءCCbmCbmCCPv;J҇iCP(v;KicX,vV;+icXvV;+ecpv;Ã8;;8;8;88388Ύ3:8Ύ38Ύ+8+8+È<8<8#È<8oÈRM'eNJI?9Dݼ0R&t-:);5<}8+QIV$YoBc?QRhx\+ɎV(u$ɉN##%SETXXLl[+gO_!fWVlɏ\K?Y:EL$.MĈ8Iz@k{0܋4f [}6ڦr&qq3`ˮ:```2^Hq-5J5Z֠Du%8 $R37VNHqxBm3y/ %$&lUSI\w1QDW-; aձP7)>QJ4?>j6,X}-)N8[FOTi7rnh`έM "}qHzQ4C4%WC/ Z.o0OF"UDJsicwo*g۴IEiI]&Url5$h7oO)2b[bJ7DfȎJ#w; =+(Oк#F'[[քoCT; HKh҄c0'ܼy[1!aiKRވ3ly G9Mϣ(tU$^s/Fc%ɐЕfY!PyJ{ W S*%a. E1Vd޹;>,%p[p&(&H:)!#MrHO҉bIplOf;P ct/tY&I$,Jqh̋-]F)' cj^?փ-_蟮;gwr*2u[s)[+ȳZ$=e#^xK$]j+ ^Ω\{ dIu |N'(ȆMKiFAuZL-3hZ7[SCwY٭{FLȇTk׊OX_cՁrEۈ,oI#UK:qsAۨ~uiEpO,CR[22I08諳@n:ZLT^^}Ԛ\{YB+>s҉LLi e@b #\ά:}xTKj(u%0BVa/tQeh@iiSY!`' / sC?[^u~R5ҫM}d~1;CXv4ȋO=XgRydHIV]DAZU޳ }ONMYHLNJ朶vBqZMᶞrYLfgkפ?FVut'ViU "\XAGgi--j~HC: &&ԤfS"c~nF ½ M|*B*[Z, [4 Cl [d;FكJŝ"-@FOY2yԸim 銢iKCz~1{}-"ȕ#.΄UfJkQxP8:/rCpW bER&!h'w0E{.>})$'t4w!)Hn2FβJ} ע&+z\H6Tl-_S%)<cGmm1#jĕ3U\֕+~(Ԯ!X,X/I7 x7 x7 p7twHwHwHwwww;Gy#Hr;G}#H$rR9 I.JG!#H$r9 BG%#HrR9) ~Qy)BG%# {\X!A7-$rP9(JG%#HrP9HR%@r9HR)@ r5)5'@ r9HM?\pHR)@ r9HR-@r9b1ls[5ls[9ls9ls9ls9hs=hs=hyC4< 0hyCZbG<őL2dy#̙d&G2<ɑL2dy#̙f6G<͑l3dy#f6G2<́4`yc25d222223####OzKLyc f ¬vX;\lXdv3<*~O2fc7r9"Opwt9X xԓp#x76'q{H' P)V!p8lCYA5 Inp8Ap⎫yOqin<Ǘ/!!!!!^wI<Zv!E,3+ݤdw^[C.$ն!YRGʴaT`L6!jq1MQTKIJu `?`}?HG:l'fă54vlKiQO}BI s=½$ sp1(vI{({aN+* -7]VCe))ͨ@S6c*a!L)oo8K{ -KR7mҒaOGpD4wC zMg&(``] &a{P}FN2 eu`)(CY {I'1c Dش=gccy~?~kXxm8XQ+&aIӴwln03i 5:mj%D,6eGm1eN/qA ?A˦0 I!PiA Z0&SA( 2lDm[AMdiI$J5 #D5(7Hhk$J C~M6gtjRВ*`!3"2HI~x!npwϟ}+ BF:H+>=}3Oڈ4kMU'[UӸK:CRigi^L3d5Lցfn}"{NUNԺ| 66G mߔԭBMA*>O=W};EiyLkMz[5J֋TXW^u-]n]tu^E|֑rk4:+gq!d1 A HOX5`$,8Ai.:u/[GtOXRS vC ]?ZL^'(v@u~2njˏQ؍ņ5bHH^.LҮ4qsZo*|? ÷0خځW?"5=R^ilN~": #-oIwzdW!Qt's z"'H52v~/ApIiD6(&S;F2n`wL,mKfΓ yؽ[[6IXAIOw60S t Dȅqmg/Rj }`z&}n:Ǹjm^ɷ.IKX/6uV%{l,,5ZfFí%j J$kUZw-aӪӥtmŮ.tk =hk-AWi&:P859NUQWFՍ̭=}5ƪ}#`z6u֯=_L*Pq!e=L+H;` ') P2|6ad>X{]=q)yCvn5{Y"A)/cݸП`CcUwG\s J8> ````AC6[_ZLu/`运}Oۡ"֑9 /u !'%Gd- ``mA$FEvF $yK&_U2{b={%Y†BJC'9d{ D{֠p!( 'rt'r+~pP0 'òtyuAՀJu`ad ~ H$%!j""mk$!#,NK C XQhuHp}M"B7r `QE JlY}!6ٺĺwkDm-$!lW^bZ,4GI" Dy&#ЗL8AeA$A$B'u s*YڕE8cGqnJ;!$ PLs.BaxD. 3qHeim5#+L<<36ad_D"C{7쥫ݐͲOΜ![i[F=,U~m\t819 r cEw䱒+֔Uv$!9Cb4bDG6,־p|w=[!t)m!a 7 8-6LmV$- i\ƒ@ D逶;nԍ=֘*3dޯtcnIwdRܓJXO50o|:Bk9eIѹm&bNcTj"UiDLjMI7,0k2ūYj/RRQ$ j-)IQ,_D"<(2. \m iLVE[J*ޕaV5ap i 5*byj~ uǥ'XwV;ՎuaǏsvrHfs) xóT,pQ:-8; v (6YwP7KuRc$(%0q8;>nv㒉 "Jr$6۰nLI5%ommɑAq%s&v#Ɲq1w̉׈\KOɍj߇k' HښIԵ~&Arkn*[Ϻ;󂵃Lr[%%$HuW8Κh5-;;;;;;;:9C:9C:9C:9C:9C:9.K9.K9.K9/KÒ9/KÒ9OSÔ<9OSÔ<9OSÔ<9OSÔ9O[Ô9o[Ö \k끁^?cp]"[JGYO%-7.$afyKj R(ze!3S^v R7DV\%2bcz5[]m iށ6QRU)X1}j% m _MgVQv]]mBKigS M79l5wO;D[,*1%|T ƨ ֿbEfG`2hߑ 4BI*;\qĔņ]aGَ*'RڵbyL_FL2$H#I .a!"$.?r}dY*GJ{砚uLs2-)E.$E4MsW4Z ڍ†谔: DTk<$_pmm[5qKRo y #!c,|I_wDuʈyw8k}{ܩK,WʗJjc5TFwۚYe3 ZǚԉL{OJC& ,_6&Tۑg?K,CZ2-Z\ɉܞT,ڒ32drwX9D*^\=܋W]oM:u.w!?RnwOԾ+ԴNۑQ %mqDGvVR.X&bn*S\On+7Ӛ',.\e-J46N{x[Iԥ-OqmM.$K9fL^w.3[9۫c^W?P?D~P'DO@FG􃽫pn'000014f\-kԗ!jWKXKde'X}l_$bK6Vnen2m3RF# llm@4  Ҷ@7ϰB:Y g!S׌d9G풐aNX"{!Bֹh< ޓ l*#dյ1rRLRʒQԛV e-~bLo|-N.cKlGi,'lLXv Go J:{rHͧe=OAKt Z18m ckI-(ƒ ը]BzL_HLɭF~}keU_.&\c:i۶ʨ0Ƙm/%g!մn?#L4xn˳Mjj-E Δ);-]iյHe > nUoc;FjmR'ڝsSW%+b"ДS.~#################"OQn#F8@I~Ȍw 9'T@Q1#n?FA;Ṙ=@{ ՟i#:QTã:;ã:;ã:;ã:;ã8;8;8;Î8;Î8;Î<83Ì<83Ì<83Ì<83Ì<83Ê8+Ê8+Ê8+8+>8#>8#>80lnFFFFFFFFFFFFFFBSՑ(d; #,QHYݒ_2,FӅI-^&j<}Bq],!,K{f]^Cma67dȰ [7' mie-X 7) ąFr)r)S$fNF,H[ڙ5 ?Yz Fc!̈wP9,|/#y{0/1f҉IpA3f#@Q2VM)-MN>aG0dkZm2 P"؝h7hSmw1E$R٩DlD B\Xk(Y<u$nޣdaQɴ\!0)XxciNBL+piRJQ1C,5\e!F?as9L^]cK* |ôǧodEZYNH Us˓o-q6;y)͑i ڋ.7yjr(Ⱥ~}5{ ;ás: O!H<񆌨xx&S !J c KSZ_YICxqt ewC_[qV/lǪ4k۷` \+yQ幱m2U0"o?1-?Eƒ/mkc{چK*Z&΋Mi[:{rL;PiG>Z*u6sO:3tbOVm滮v;MbSg53ڦU/ꃤ^;{31"H#|V4ihex~l(Mmт?| @GE57҅ ǠYq!Eg  I )6&/~H%W5\-m&G5kF{4@7M QmJ#AvT7g.DzvHOǐCXR|E{ǩOR0FG8Ad끁D 6%g>ٷԐ42DGFDa:MfJT6mǽ4"DT+kЀ*@E%QlTf{e)RAk.lS;JnHJer0C6MR* .\، uDNؽY$"!DBDf4; ݹ1dQYR`,A"@!Kv>%l%}``-,ܑ "0LhSPݷ)o\Uvk%i NeԬ҂k OZ(ٕ%.ScלΗ}:v,.9 {"Qɫ֍Zv%.wfdEgxm4I{"d?!%C)ʡ(@ DYJ&0d.Q]0sQd"n"զl +-N-THe`g".d-IX H-ƎvuC)QDeHzCOSoniPC2V|)plN ::xDd-kٛ6ZNbS#)f٪H*T7eFXZ\50a\ @!E-4$Jc=ym^OΥ?CzˣQ&* !Fء&"dx)ku:(ŷ '@DvѫiͰb4Gv ̊>.fzAr3."!&nUxS7{Dk-t#S-Uđ5& K )~(ۍ jhiLSURnT'?͔_!&lJ\J8쒫#XP4cp2!YlC7,>bKbATj6ګfԯX%lbr_gscH6PIh^^(;L ,HΜ6 oR,iz2ꖴA\` :g&;jvkQ}c%*sbjii ۃ9VȚKNEsfJjuXbm<11QaCPq8e2CP& &;F;c`tɖ'׼Q$J#lLJ,d'RQyU]-V[06L;YKŻ%*X-AQ;e'T+&WRII!#pf,tfs:luݤJ=_i%ovLvLvT;&;&;*‡aCPvT;*‡aCPq; u:CX,q8U*cXq8E"X,qGpq7 pp' tp]GtpGtp^G<.xys˞\燗<<-xyk_Xǖ>_mȊ,i9SO)vUnWP [ae*LgaԈtMS QiK2P[[j9_ʈ sO:FQr0R+ L)a&*r]f. [& KvGvy6aI%J6L-3 *\hHRuh3pQ;}g=AɶfuA 6Qip0 6˜3WFh84ݭa"cbLZܓ eĭ괽S;+]I<.بKq Hu5)F.ŝo_Bשv6,jmM;3y-ŃKe&<_ jmh_@fDFʝ2#RR5<+Zv:ViRe)M!1R#@ϒ5$Zkx*"5q [B|;5WAKa50/CY:.Z&QPl )Qd^)QuJ`B[-::UFm`'W:Q~Y#%e ILÍSnujF@Ge\ŘqtҙjtD{G>u:RA:Rl'ZQ=)Iq/ѣtgjvM k4DM+%yœi4g! ˑWWk\lp\Kd/JdN,=3!ՊsͯGZ'+d4.+q;X KFԡ:R#b#Zp{gИ\wlkk)=!e#™aQ Wcd橖0Hi6J yƅJm4^מ(ʕ$$ZyȢ9uí#u{sLGphj>,7FY8QJ,tN< Z(7%~u#1O.clWdM'\^Dg&k-ծؽ!u7@jetC#MMGPZb^N}%w\RX545A)+?d]$p#=J٩1O=mmRXS'D!Nm^Ky3!BS+ܵ9L-6{Q:mFu6^$BuQ`ŹLgt_kދ#!KxڗI2ވ3lyC#inO>&j`d(>⥲FzKjٮRͰkMꡞ]ɖ'qp$ƅ=M{UYjW.i9?|ZP  &ayT1Hqma1g󰎕*ųaVNki &֧bCVk"s-Z;b)eֲokJJۗ M,H#5Ym5 Jşq&UjQ:H+K2۪RoIu(a[4r#'-+W.GjO!'S\:hC+\ BzIrlaΗj-giZumLX]>?u²K&Q#H\I}OU.2k3TsVܢs]?S[ܪtVƬsgj7 jMQ-seD= YwZa"TIwƫpl@k^;ҜnnmA@ڜ)Ĝ'G g*]nuI tyCcˑUZe `-X'-2q1@ ɪj,2) L6fƛnih*( sIq{ҢʇXFBNKMEL/.YnMvb]% H[yv$F*dvm\C+BֵOH?ӵT I6DhO)8Q9;)Sq;ReGdDyn~C`Y=^Ms[lRjϢTwkfFhuf]ͭ6Σv>WZ ތꅚd&/ա%3S:zh FֲZz;=+U:CR&D!YxXT0-ui?a-sT֡%hFmYLOH-M:)l{oޢ4K'3ke.ENI__ IvJvvvvvvv;iGm#0XZlHؑ#bGm##bFčFĎDMa}Fn;6$lHؑ#bGm#bGm v-.hdbCQy+b 6 l@ؑb 6 l@؁b 6 l@ځb #"*W56 l@؁2Zl@؏^^g66666666666666660C 20# 20# 20# 20# 20# 00`>}}aaaaaaaaaa1Fb3f(Q1Fb3d9b{8^jc͇L0v8`fcff '0seyV{bxy;3g?{;7́KƤ;?3lrA>o`rH'(ddd X6ROpՔ5/ u[z+O.?p^ ʔ>Ul3(%KMJf&ѵ)S1jZJW'0D=a[d9=6$}{g-HB[J}HK/f9&S)CjKC wCee (=m(2 ؔU"tXjR$h y#F|tNAW]RJɽT- t~kqռY$g"|hfĦet&ZP”0f0Y.0>'Bd{2!IZ=I?lZf1 SLi@oMI֎6rľ!b9h+>hl3X~]Df\CkT}#G]jV6OJ N<#PW. gQIM3;;^5*5ϤP*\^&K:R[1SS֍\Ïѡ8zifu)gH, f]1jI M  HR^4 @d}e'>u#l*v?`pk GJLI"Qd%j#Vk>mx1Fz _5)ܹZsQMm՗gLOtC$OR+{i6,Mٺo1#^#hB ڞVY'Oܮ^Sװ<%{ #KPv|XVnQ`[ߺŶQHJܟ8@l$ !E% lo$!S14^ s(r!6M;w+%X!nl ;O v&bؔNx1w]!W(-G_Z5/)#DꛇELk4E&%mXΠ7lmmw*4mI^mFžj'#kO>y(5]vqMSQX۳Jiw$K=Av%?AdHWc60S;#$47R Gx{oښl|JJ2 끏oq-$hi[3I,g\lb WSz^ z^cBj3iJȰM?kD'gN@JKVĎ&ŭ#8U"4Fa*֥zņhǎZZ'XCsȡLo]3[:nk\GgFi'TҾ+"X5}@݅5v B]cA 䐟qk1 mHXp#H\u2^?/b[F0Ͼ`N P{#1%tm21nKNCfclf0`/\Q# 6A҈lP&Mfv0eY=!&@?{ lQ( ?3l'mp `!au˴ǻN%/(n-Ư~?s@H%>{Pߐ׺h) L}A 0$+ϡE͖W#mF +}:/S@ȵc%n#"}BKeApHIMn#k}L9zO W#c\V5/8FѴA$m,,{_Mi;d`GteS'#ۚGU(d-$u\T 02Аm}kjCo _1}dll Xd֨H" M%29a3q;Z)&)RPNpNv0 8N Mip/i"<Y~.IIBʖaGc6OtIAm_-BZ^E]mpd27{$TV&ub+wdžۯXpLPL7?ʢND e. (%Zy :R6mQ"> )zz,E&nivFI.{ YƃUwTMn#SUJIh{um3#[~vG%r[#Kkk$g-.Y~."@``cD$ЃZ7v!8QIh_ȵ*rA7XnPo1QmC|rkm$-rh:%1Mdz-mפv׺=TS"u)CUjmq.a}QW9+iIاotm-鉯 ys\5#9jdM5z4qK ~[-)#ս `i^Mbu~. ]14Fۣl("mZHea-~\_m4F.myM'H 1NЮho!/:JL~R~bَV>"N2 츯G&Wސh7Vͬle=K9r/j$ʶHQkPxڋM8~]ZSj_\M pFޟL25?B@z^z  yrw<@H7C.[?v*W&< H&Թ Okj^yuƛG#6|QGjCBAhb,ܤ{+3O X2L5a&޵ cry(5^khUkLju,jv3 XQ1؀GD=+/!|mw0jzkSc:B3"T װ{pV/#Lg]}`„v Mw"rK⺽JzD5dY7a$dX jbJm ش6n$"lT?5*("Oa&ꤒa4Bd2cƦ\i᛻6}" [6}aI$HQB4YaW%b&,JBmb -|p %ѳFt q]H[l`n! Jov19I}1{{$i(~ uTpLb4TJrΗ7, z-<$,I=f6H4Fd#go<#pƪMz6[krɿ_1 APYq `֚dέRbYW!_'eI>nZѦRжKc֮ͥDt8酧w.4tl\xd"opNȲ6ﹾ}*y$UnA*K*ݻؐ=WhBfZn]֜=v P8bj-/ߤ 29 W MpH4Mm$Ba<&m N$q*%O^갔ŜFja7;‡wUkg\Ym%+(My "Wj#~\$z Jn )&tI:> Yַz3r/[Cjf˽(4ܭF0 ЈC޲.-:`````\HHXycVغSIGm0w0@8acH~eqZ de))0G^$2 bɲbs<MAdAKoݠha)FEضY$g3O%TG ~LjsbÈK>(E-ȈxۊRIy+)~ `Xkde#,ه﯉\|mWm3[$S0PabꚌRn$|gdQE|7?LbJ*""SKXL)fVYz{q'JIn[hRn+Op]7u+nK2ϣ^aKD9$'&Oh\mʔiRS?im AAM<-IbU/.ʥ!O1Oy;,QpU*(B)LR?LWz?݅{KҶ2%w}zT-t5/32bY59GC e<&,N5姽GW<:^uOECAmP^ QB%']MhaԱΙPT=c+XoCi $9)VӖȤډG/C@΀& ZrJx[%K3X=R߼s6 {ZB) cc hy#@P q)]o'cß!ŦG42%#4A ID#Jm&eŒ£)?fMtSqɓ\h ;h/xiJ y)"Q,*"C·%-H)1+~L)Os<k6 "# $-+DU9iua/b}DQo`E⍤Wph-rC=&]^Uq5Xu{!t6yu{ih-I F[O%:<-ch)4c"Ss2kV!$w' k! H.62FbT~he֖M;}|Wԥm-"r<ɏ;hJFo<Af*4o)k5$;v<#Vڞ5lu־xNC^5.#s+Fr-#5ڥ&ShJ><uwziVq2U E^B;QOZxLv5lX JBIϫ=s4[$ud;؉uF7 }9둼yC1՞D6J!ADkOѕ,9!nEMYLaj%i)$LRId՜2fGӯp,e;SIFY_*q0?9M_5+q2r kRцhm.py퓮8k *&C51ZQbC~*vZUKdu?V˥փTӉyy{gigE6BHAVO EXNTr6Au53oD]t!6vk e=a{} tun؈nY͓*`yׂ}>M'aB x'/6xPCyƀK?kgrl~in_;(tz=_5&mث?ZT{O>?>L@\+=l8_1ɷ9,>d1}T?^,#qc){~h5= YkON.o PԹa٣~jyg؛#[+>oǭP"Ye*ʤ`{[a]U4f;s~݊FpYI_ު Qou}' VĪ)RaQNpq*Vp(%qRvǚ 2n}#tcbK)Gi@v,wOnQy*;zḓ >Lrr<{gܷV@s'=4)%s+q^ ?F#hc7=$$[uOC(靊&ة7.QE{*zHinFrϊ'õ^ ;Oz92k>*8!F,:7ਥ'-gvENM颈݁\R1ԩ)p0 OFR;lLZ97µ G4Nhan Gưahص G 9ߊnӝScBkb&o+~BߟiS<{򬝳r{7pr/PNg3󫁽-o.}=ޜ.1pBSahb9|9GZіy⩰%nKw_Ѝճo7< G (QVJz9\POk Zv#S:g8N0M7P3ܧfarbܢ6_1!ֱa5o2?sg ކm7fmfn3=VtQ*M3_Ň==\{fJqQMP6_2Qأ [ )7+ha]+\ep}#gʔ{%~/ڑb+WߴCψ(>S!U2TRպ 9+qw9(o3d+ovgm eG߽14IXJ&\DtFv+Eu+fSe>𽚮ǎ~I/+TԕA)j'(n *[#eϚ,~YL2YRW6 Q0 #Vɢ8"ez.cV!4M{lL2sAVLx}vy1D~]n܃tӱ80j*ʃUP9M@J \Pga܋˷uRJ\ԖcϷ1WT8ҵs-8J&NcAsFՕ)u1n9J9vvQRd;x?5G^hp[vRmC\!=Oћ8&dO{ su*M4EX杣HhUrAw(sH;*xߙ1)ڨqTs fehM8rYG|J;:el7lU2QBw_贍tRo܍ [Τ"T<;VF:S6aWtI)T@8tB!Oi4| FeW*j8hJ>rvҰ2G*\1`cp23Fܔ4g͍Zq=`\ D7f4i+Q2asAHұs}1ֵ}1ִckc6Xlo'{rw=Ù?v̝{r-dvowW2CEʎx<=qש4&"JYG_Y2 X4olM64 ⛵}*?p6 ކ`.cUqNS\#n} lA3t *g >,'p5ۋ{Ok{ܥ5A6lg&g~ZZ<3i8d]Ie^$߲X"nD$hRQHFơyl4,H1 oR7Czm4`X^HރP Sœ fxg'%#ݚ] 4Rdhv iTR|\Zt(|ߏz y*"{O-vZ?=%l{qةr5o,n< MP| bvK[ZE ׶Avpe9Y*+# WU2E@ke!`AAwr%# ?\;p:Cob Py|xەMF_ SUg( rkI_͜Ḓɓ4_eڥٚp;BmY*!< LPjdE+}vlH}>* )&I޼'ZjOԐ:&QƢՔtM g:vL-f S<^>3?VjXqjjCdU]DfxDt,^wzUTDd̪cy v|vw\ULi ܆#ƒJ2d@M,aseiZ"9<biئ&%HPM3F49e,#dcPe z2U]6>YGۉR#<Љ )kӚRfE#0R$%Nc\,Bͣ{cn'n^T5 =Maf~ʳ$UQ~{Tq8TYp,ɼ/zzv e XO~3sbpMj&./W+?-v!l,R 4]9vŅZ v*F6͹Q֖0co+)R"V(e[+˜c|l$ wK('Fbz|pýIR[r6}s= K@+  e[ֈ#x{)\֓!#`t|VI'Y^, W?+W;jݐVu}'5x6'dyLHL! b+OEVͬg2eCNhٱSݱ7ر\"'¨ hۣ;EWTk 4sjׅ?+*ʡp04p5l$5dc0(}Q^( W?+¿I_Br6nNʮpjto*&٪lѓb-QPU]mhCY&AeximMG.ߒ^( W+_ExWg%kgW+Q^('3FETѻ}؃TL= (a JEVϬITj' ׄ+Ÿ?ʐyvx?eJ$wL/H%YR]qhʖmig{\ hNJYU(ڜT;JeexC!YSϦ>^B!OPg#f<[Y `*j+0*BQU "\[rBok*- Ќ6׿K]%1yG Nܥ:w0͓BXɎ{bw&]λJ+eV CoҴZի!MV !$l 1Dq̴%h l MhtYYS>1ؙù?O'N^7cLO>0AGOԋi\\zG$ Xq}܌"WFz~Ǜf۬H F!bc(VI0b}a2Q{'u:GֹT1;feo@awN~lg,R-,"QS㚓Bkznl[, se*hrT (U:(qmLcclyRϐySɾ)Vcg<&) I3ߊh-N FwY hpYFJg܄~$S&dtoIfMsZڱ0 yռl`7\17519K)7>nf*85/5 ,RSP7زtf0o,!a ܫ#щh k,≿$8,nv;<څCncZ#V];V-4t"[{|lQ )L5yo`xOQMH§Q1j,$-F7H.,|M,ܢxϔ Bsdn/ uH`ʧv ZJssK^'nM}+H3QY1ssikfxe&/f,7.ܡѹeQ77/r7~zsg `6ܧبL4g"28g9 w@^x懌.U3BMMɂ|9ryًCT ܃@ډՁvqD7ix0 q9 *YMA^@r{ӡiB5'eeenMa!/eZB\naEY[ɘXe1D]hMA@pZ݅jWj(S @[`d.cw֕,O")ʚ]72}ggx(ϚQ5zϚSggx%޳Y^ w;|ׂ{>k5zϚ;ogx޷[^w;|ׁ{>k5gzϚ;ogx޷ [^w|ׁ{>k5gzϚ+oge?ubh޼ [^w;|ׁ{>k57zϚwgx ޻]^wv|׀>knݟ5~AQY/]06. vYC i>݋&}x ޻]^w;v|ׁ>koݟ4܄{濳4><y^wv|׀>knݟ57zϚwgx ޻]^wv|׀>knݟ57zϚ#wgx޻]^wv|׀>knݟ57zϚwgx ޻]^wv|T3UVOn_ekh'v|׀>knݟ57zϚ#wgV53iSpvT|ozi>/;v|׀>knݟ5GzϚwgx ޻]^wv|ׁ>knݟ57zϚwgx ޻]^wv|ׁ>kݟ5GzϚ#wgx޻]^wv|ׁ]>kݟ5WzϚ+wgx޻ ]^wv|ׁ]>kݟ5gzϚ3wgx޷ [^w;|ׁ{>k5wzϚ;ogx޷[^w[^>}ogx ϚA5{>k'|ׂO[^ >}ogx$ϚI5{>kG|ׂY^ >}ggx(ϚY5;>kgv|ׂϬY^ >}ggx0Ϛa5;>kv|׃O^ >x4Ϯ>׃^>Zq׃ϧּ}?uOx<]kP>Zֵ騣03]]jmu$%n TZ-OBj+SZ-SҵNtSjpj̵^[j+VZJzQ:tJt_h:Vҵ~ ZJVh:VB+-JЭЭJJIAF%-ы܋l.+Mz/.WXSVFK-*-/B-/Bҭ"ƱkHkƱ,Kı,KƱkı,KƮ,Kı+ m °+ °tJJJJtJtJJJJJtJJJJJJtJJJtJtJtJJJtJtJtJtJtJѭѭJJt"ȹi5i5s{sXXnEMp[!`G0G1A]7+WG3y`MBuf"˛jhkM{MSx?C`OvQ lSoR lRO}]jcM@JkW6ke܌pel?+ta6aG#%W[6 2?pʈe8XO-!:7jeLլS|#3H n6(nF:A[iCCֈoi 'r)~unhFWkZwXuտ&sY[W$8sȔs_69NOTSY] g&".ݘ+HJ+z =S ?v-Ail[fEmepau\L-uYvqYF]7O:y^v [ޣʔ^ގ[~ 1,TX75߹`1;z̑h*iIۂnYM}t޼9Jan߲*M}s ?^Ȥmdr&m&׼;Tܡ,L;lb ޟaý;)Bw%bv/ A{ W n¨ 3;~ CvV9v#="yGyG|GɔS{ W&ȳmF2E^xWF|[D U7Oa*LV8sMY7x>?wNbeM2y7юU$D) nT2w5;-uÁީ­>Q0MH\*>"safcWT~c~ͣQW?LT=?~{O&TnUKo;;~iG1rv7wۦަ-1Ub%\5R4s{?e=Ly U9sp*o oZߑ>@Cɺ<('"cgެވ'`V,֌8mOna( ߜ;G5܂ nW{?V}~,&xf`Z: bxޘMݹL*nq6lck{ д߷zRvﭽ5q$5 Q1Uq'ڴf{U,X=%iZ9U=~*t~>(~eQ$|g|J_t {f.mbn5dHq ޞ9n݁KOqH!c(>۪=gVx.NgjkGਁ6܁/-?&T0 ~v}nM$>HO('"9WD8EY bSFr#Lx][>ЊކmCkmWj;+g9odcd斝 Fvr,rgؘ5#~EVΏɟ_9WȜ99s i&j77Όnˡ+O:{ޜ2thpKwh(!ثd W[u|Dآ.rn;ˆd[$3U-o#o(,@( LwϮ6FaV[ʌV[nM|.|~]s/l4/+WWD캹([0Eb-ڂjIFl O]ei+}}i %$%rff9&EY?aYK*Cww0YO+VRw0OҔƄdʹc\G⛪Tvl='CJHo8;N݅C%vSIns|ﯗ+ܼ.CRTQ㷔krmȷTy9 \,#>X9܊*<-#.@"!?#mn^eQQ,NS,esc~`,dbX)ȇ1ؙd,GڬbQ:HF ,8i o_qk+Vw%drCqmtwM/պ(I#ʺکp{?I+y{TFɵPeu}J};G*9pC7ǣ}q w'(e ^(]:g3v~Dr#v)f|_LMPٍZܻyf(y3h*袊9ȿ&mNC~cje܊܉(X|V V$_XJӹ20$u$͚-SU6t:U_jS!>XvZ)rMU#PV%8t eNKwbSH@k}{Gr#ͥeHt/7 ](5(#s͛dIfi-;bHR g= mɭu3s΅c%kӝ.8BՙcrFkyFsɿ$C9ϐk~yR X~@6lU<#9l1{cpQd}+⦨B=fC^Z [̠KrE TS08ESj9R~ܭ((.ð-m>@D򏓿$"9BL|ci6_j΄ϵ UY!_ڪi0_JhEre aݨ;iމ0X2lF7!fK/dw-Ŷ&=NWJi'Ō۵ |2$YSsZZ=+(c/;Ji n8-|rjM7FGHq8񞒹 4MݚP+!K9 >D@(<ʿ"ߌZi?rM;oOܴz妛w-,ޟùiM7_heӳY6F2pUnLO9BD$Ae.Y fJ3o=M7YP-1u@Wlrʡ< Yi7S{QOS{R?,Qx?lܝPc5 k [UlUM_ X,sYj1KvI{%WD6i\>iiT3p/Ww"7[USM7ڢ?rؘClMbH^ne姟grNރ\f20o(B?PPevE l> eT7eZzX{;姨i=aZyaZzX{;姨i=aZzX{;姨i=aZzX{;姨i=aZzXi=a姨{ZzXi=a姨{-=G=MZzXzܴ7i=aorz姨OQS{-=G=MZzXzܴ7i=aorz姨OQS{-=G=MZzXzܴ7i=aorz姨OQS{-=G=MZzXzܴ7i=aorz姨{ZzXi=a姨i=aZzX{;X{;X{;X{;X{;X{;X{;X{;X{;X{;X{;X{;X{;X{;Oܴ妟w-4ùiM7Zi?rM;oOܴz妛w-4ޟùiM7Zi?rM;oOܴz妛w-,ޟùieK/ZY}?r;_Oܴ套w-,ùie9'1s+|}F4WUL}tZVrmԚ&wMY?D摚F86ՑK .F'="=nڲJc<}66idZysdhnmLJ̵ s ;-<86iI;s܃mDvy*涞&߷#x)ʮ27ީMJ' rl"79TYD} 1zb,6{v(Vݍe[ʖ d1p+xmⱷqJb'W=>¼'IRXBXcr KrkQ oZhܧ=Ps]̬rߩQ[~L|azS9H1e6<(lѳ3Ab]M=5>I;ĔHMaCb54]dl~FS5 _iXfE'DBytb&71hv9.E׽bSL p㧟aǰ2C eA*ـW ?*(i2\_=6%To({C>uYQF8nM[Fg>t=Kv⠤M @[g"96 R]Q~gn+%~ |ɧT?Gy/%Gr)1,ʅGNӽfpF4p7b5 T-\d1{̾>K3[ 8%B.sO+pDJ|qXmND<[죨w'Fd(|ߏz}J+J+H+H(8;w) dy.vCb4 9MC4|a*jtvY;ϗxXU0ܰ"LMY+Y-*x N[9ۂ@_`Y>s@g8~Ң~67ǧⴾ>l?,܄1-Φ1퓭C=T#2)]_TyR0Im(ӱ+}EC@вSFyOs9VѻL9y0@jn }hޮ6f&͔6S: )&I޼'ZjOԐ:7ı+hc9Ӳak1mT29Bç6Ggw,SWJ$o:*#4{Ů8(*#d{F#%Ne) MͪW:mR=i$- QaM8T&bm p'aMM K'ظ<^y1ybs9UuOmC6%,}"I;satnv{6LUmy 9c"Fo0Mz6{fG? -uQRU6 Dž C*ٛ-}}-j{@Ւ_ OhpRStM\ ctTo: Nk*IR)ڦ0lZJ6zyNJ-Qfk-޵;{ ܛ)ǴoV74u3߿bPAw -r*[;\Zq)_.!t*yd5O94~lg*G SY:CUڭɡR[Y&y?9ozais{}9NJ5\`b=wd)9NMn`9fw&l9A[8S}lXмV0/cةɗs咇Y9чVez%h5jÞVScpܲXpLjpt"Z)4&&]GnuN&utmܕ4aaOQٹkrl/utH;m%];z9%),6ϔ* a&UImQs(˅,c$ߚEpDN8!se/9dրzhe6aTo*ڜԟ^{PY,X@jU3ͻI*_US\vPtQ~* +,1!S9اac|R2 xLR,Lg?ф[5KW&S㝞hq?ݏh0+7[yQݥsMʧgߘb#韦rԒN I'Cad5<=vmM6@pXO:y>(9$rmȲ&o0M;Udq #^CP*/u%; s}'0 YX,!` XQF$bZ%(DPeɲ4ߙ gnbԖZ#T_j3Ev~t^YNiFj6-)9:(2T-!!H( ؚ9Y'_1囔Rᜨ^|npmڝ[eXUծ3yT+INz~Niq!Pd@ɯҥv)Fj:I&.nu-5-xlх۔4:795rc坊͞>NȾEJfY9OJ~w4쩤yC. iC vS-Yjw 59$#J+fcZJ9%փqaݹbm- `4;3 0 b۬Ǵ|UenE|a*ڼɭMT.P,-jSZ;ز\rzg,0imвF&@۟bZgafPqX=*,FA>ő2 V}d֦-5~}LVo9Юk+++fi!dpZ<;Ozԡ{֥޵(xvCôJjPp=QiZ;Oz`{֣{֣޵8޵8޵8޵8޵8޵8޵8޵8޵8޵8޵8޵8޵8޼{׃zz|Y^=p=gx:A~=n^z޼Ox>wz}?~[׃w=n^zzu?{׃w=n^z޼Ox>wz}?~[РB; Gw^^z޼Ox>wz}?~[֡Ox>wz}?~[֡Oj[׃޼O~=gj[֡Oj[֡Oj[֡Oj[֡Oj[֡Oj[׃wBwz)wBwz)wBwz)wBwz)wBwz)wBwz)wBwz)wBwz)wBs֡Oj[֡Oj޵ ~Pz޵ ~PS=nZ?=nZ?=nZ?{֣OS=gjTYZ?{֣O=gj4YZ?{֥OS=gjPp=gjPp=gjPp=gjPp=gjpp=gjPp=gjpp=gjpp=gjpp=gjpp=gjpp=gjpp=gjpp=gjpp=gjpp=gjpp=gjpp=gjpp=TiZ;Oz!{֩޵HxvCôRj=TiZ<;Oza{֩޵XxvCôRj=TiZ<;Oza{֫޵XxvôVj=UiZ<;Ja{֫޵XvôV.jp+ViZ\;Jբ%jp+ViZ\;JdLaBkOcd޵^bH9Cf`C|S89l^("l/zm.r #fΞ`lsAsu$x^tl ͊㫰( )XO: z,(kppNl_0Dt^ՈdG(`Y{s_=C6EBgf>2fb[_d$%W."E&6KՄ&lt_)Zk dq'sg:V1>g?3^Xnek9 -"b;"UTK3/T?hގ["r7+'Ri@NxYG+916Pe9ekdAǂ&ufP婘$^;{E o࣏lrDѾ9G,Dl ϾǫTynk4l PxW3RMζo'o__,r%'&/6ޭZuҏ;,1CTYBF/$}mC[P`Ci{uuSH@zij2骄1OhE;wrmFrkZOa*&y>jvoV}~Cmީ1h6pB7[v[zʄyb`sw)>p797S 'v{6О$f27NI#==4dB>W&Q$|g|J-M11ǯEslYcnϚB9I@ wKo@;{hTiޟ~< *g'#g}~d2|A۳H]YA_W:(E/QG*LfX6FdܓYuBSÆAnՖYR'܌B>F/ԩqUA+ j hk[SRT3)6ŏNlÂx{jРReK]Rmթ ⠠[G2mCRTDqluUm#it oTk? ~~ݧ1Nʲvx޶c ŽvȾk yA;alή\o` _zmTx-;*uDF6KX9rp35lIj'FYgWU.M~WB7VͿPt71P(G0W+ZG-+֙LeqBW?5i؍O|5;16@1C0{rنa9ru|rr9 3oC663 f|I+z(su+`2>W)~D1d}av|C?귵5xqQ")uڂϞW g܂9p+-~&UA'}ms%e?]1Ubz]S`o:e-;OGEMM. 6)a!dH'|NvhQ+[=bbo1NNMW+~hC8Wb'vp .V՟n F̡PcΎaƧȿd&`.y)atѹcX-t5TWh`]%dHb|U\(,hly263=usr9ﳂ9RC.TnIݕTdVez)wc&8 &IrfvPۛ/eMB MnSV4]]]bWD5+ݓH|GvtkUt4D4=W.fSo $Ќ<6vlQ}cT} lJnkl|TԾF}z$N L4~os:)]-{\CkoRZ>Oõ=N{fI&Lu~NNIgsG&x;WM; umS&V%Ҵ;yX>*yA5bkvf*ndܑ<':!2N=UtDE1]?*IxŦ蚺7h#gQةtn9,O$ßܴEj:ɔbޢty|'*7S'+=ElpV$jͤSolӚ #C&0YJr"pA#jn,j28{: ;ʎF4B`B+;rsڷu*>"ݪ6Qào?㧦dq@4-JɌ9j|NRݻ=喪]#߿zZ{A{#=6a_\`l sb7cdjG]7Gk^'(M<Zanu2%;n]e&bc473PMrw87k5pgkɟs=#,r:xNօW;J#t!hG)QIB9Jv/zСaxtܮx+@a9(<ݥ83\7;r{ ۧ+ ;R\aM:GUT7o8vҁU9[ _޲ڪLQ4b ߵO** *ݸ{~C-鎾E7k._묖&m ؝檐 eI:rVQqưEG5YU>!TIf']CS$aY:Y~C'),;./-ɠv*ZTZx%Zx%EkzKX_icتl$owIhdtg AX38fn1d`4rpD s!+s-jZBId&k+ۛ\e$óU'Uάt9eZhr,f94梨n>}k!L&nޥI]MZv{V%]4nx9P1Jbprһ#1>7a*x XU+pgok*. XZ^a}c_a&-7 k1zKXZ^b%-f/Ik1zKYZ^b%E-b/Ik1zKYZ>a}%-fIk0KYZ>a}%-fIk0KYZ>a}%-fIk0KYZ>a}%-fIk0KYZ>a}%-jIk0KYZ>b%-f/IkzKXZ^"%E-b/I%E-b/IkzKXZ^"%E-b.+XZx%Zx<|V>+OZx<|V>+MGiZhAc/ʒAq$C/o*XXF=^GHF'M9bj0?w=$.o' *Wbۜ#N9h-y˶ 4}t1HƏ>Ls?Zܷ=|4 ppp=k spg NkOyMSˆIً\W'_;ۍU(R99Ӓ7(tnT09VJCT ͹m (0;;,D XSj9Qhj~YlG&*9< g5E.rO(B0o V܏ՙoK+!jp*U(cM Mj o:k[5$~=\:H1!879v ڬ!/܂0۱b*ؙQkYK W/}t)toMX݉*xev'6;P,>*ɿg_-c bY?̚1H"g&8Fh@Ĥϡ6x|=ܙم⬧N<-3)Z7ll'eWbJf3 1Z S©h!ALҍ$yd34Nrӹiܣq RjeZWCvO] vg#86 @Ձj"Yj<>y6yMM,x܉$ 2u@)5f<խ928N<ɳt`}]S}&Sg ",mNIϔ%{0&:&D>;}TV1`ܦCބa+ gr+i 6ڥwr? ȨxpKPU ì'O?&dE߉%-JǩG(`IjZ0nL@PD盍KF0\%<ycdOl k8H)|o*C{?÷d35FɃX08?^xq)11 N~|-P֔cSGGfb1[3Pp jZ&'7ܲnɑ%rjj#8:4;w"O*%w#Sbų<..7*8&!vݲ; -+Jrc6<ݮQUzk!svr+cV@(V35XHqE*3e雟,j:m\gHAmЦ>ņ6tn;^nT4SsB0lmPwB ~$FI)bv6'wF6oRS'ݛ;Iüf=AgYZ7c`क ۨ*V2ѺU3IS!M$Z?fsCr'5ߙ7To@d5FɃ͔?D`D*хG ϟ'~)ekF e%}ۇJۤzNl;YN:r}-F7e[CW1 'EAvev )z(laث+8ݮRԖ\%PRIR$Z,3DXqED&oW z)Cy5y'5MiL_h2 zZk>ՍsSkHI+][n(me9 |\yiY3{N Hb>r۽ɁFiPxQKsiZ-vW Z->nRݘyy\|VʨkiY s\79?>i8oՓD,V`ilJ?I :]X(D~LDQ6#lJoTZG/%Qr`e9d9ѭA!,{EvC'ڦ?hAJKOdﳮh.2,<A`Y^X$K*s [yw"OpHRoLإѸbFipJ|1!f ysp81Nii-*Ga۬8&hv֟z zDufFY:QNwbM5qJu[;rcmK&ɢJ #ɹ+o'W%04\u"po;1&2dSN*Y ؼ Kx{VwbX⩩[M{s\˧1,  A164DߒBcm4͔Ҙ~}*`شߗiCN(Y;O/~tM[#J2"6?! .<'xqBtC e9dm"6fqX[kCvENpR4BT&v[cG;ۛ#Tzw{B0VTwpUQY\4La>;w~呣#O6j"O nTyEL<[jX*0I >M1M<upCo3 MH֎ oy#mԎ_f/xsTO]Bڦ `vJ+$*(9Oߙ_ Y˙77>h~rsCbtb[r}(T#dX-фesV:>d8nE5mɲ,Z !h\. F 6ۙho@$xօBx3g(j|\)q`ǰ;iThb <71Ykɵ-A4lD79-7 +>efv*I7<9R5X\ Y>UL5E9 p7V+ Mȫip>)8se P$!'5󖂋HS1dgUMpY,3Ber>EM0̀ CG X}Y,S ЋX؃aK9;r݋SjRUJ9V*ZSjrJUʵ9V*ԥZRjrJUJ9V*ԥZRWRzBQJ2fZQj2Feʵ2fZJvAN&L飳Ozr,9*fZR I֩2m-Bs'ٚ2fZQj3-Fe̵*eZQj3-Fe̵2fZQjS-JeL)*ȢS6+'}%CmQjRJUL)VQ?UCTۍfRvh_߿gPi[azԦZRjS-JeL2ԦZRjS-JeL)V*ԥZRjRNUJ)V*ZSjrNUʵ9V*ZSjNUʵ9V*%ZTjRUJIV*%ZTjRUJI*eZTj-REȵY"dZUj-VEȵY"dZUj-ZEȵY"դZVj-ZEHi"Ly[_mvpj+Te24HRtWj}+Te2zVҵNt[nSҵ^tWj+VZJ4ZZJV҄+WZJtJp(Et"ZZZZbщ:<(2İ"Z1{]=]^m%t ҸoE wHeZ^Z^ZUX5i5cX%bX%bX5bX%bX%bX%!uu~MYYYaXVaXVZ>Z>Z>Z.Z.ZZZZZZZZZZZ.ZZZ.Z.Z.Z>Z>Z.Z.Z.Z.Z.Z5Z5Z>Z.cDY-"Ʊ"Ʈo{ Nk Ƞun ~7r]l(+vp}u؃PO;,WiN2 1DYsmM iOio6l ښyj$ x\A I+LiՈiM|8[-u9쭚هn.Bߔ<2Gb<"a‚wW[UqSE]oD!sAnN (ϷbH6;T,؛w ,iCr#iZ!0ȴMԒ9-u5[g5-o!o~HG9|NNG9oCtE;U܆rl"騭قĈĬkyЎ6"O+foF,Ed7ލnPbo 6GaX;BK:˜-Uy5/eo(9nM"3'yyȔ|E9 e{l1(c-DY]]EtonI~Q@YdMMjz&:V;Nڀ;0W`, .FiXFҥv€#`X7^@ObZߑ>@Cɺ<('"cgެވ'`V,֌8mOna( ߜ;G5܂;͕xa`^GozfYy Qeʉf~>밪}=;Q+ӻfYUU}VqҍT#Ov#l.I|<ڍ$ͤ{n4Ps݁eIVa6OS4Lnҟ,t{&]CS<b7#W;ڪ2lUC+ rߙ2m^6.!CU)uOQ4,s??E'|񍾾Jz6+'C~!8ּCyv0]uSA=^-&0֙7\߫bWvM&?{tP /HwXvoDp|d6&;~Y;/ȷ*ܣ3*'"a1Ü-0ZFZFXyёt%i@vor6ӝN nm3{l zٮEՕY .哨!V u+49(2Md78z$B;8[ɚ#">sBEhKo ,)alP亸A7ߜsj{ucghU9.rvN|f,6wR7)aoL %}9IFTC6SSno*x((* D("9wW[F\+ p&+mEg +w l[WWEnEZS"iQ,fSDqN"Hr9*H bCzNb aHk-PND]Xs#Xo9ܫyQܫr-ɷRsϗ<˺?bafP\p*蝗W% fW܅{PVAXr݈ g9&EY?aDl7 ڞ`RSloUG@E7P*#n=Izy3Pq:nMʏ ?>yG9Ǔts*P|4oW7( C5؀^5줭^;.w♖{L/Dl؏7'nCnNp@D欩wYbjG{Nh`n>tCEM (-9%bP(n{S?n|OT Kp߫DꁷF|qɯѷ>sx_xp< >3v=<7ţ3#ߑBnª+n(g|OPʆbnqDy{o>Ps D99¿.djsܷbyⅿzڎlN2~oj.t/y)9a*ĉZ[FR9 qXk÷r2];ôvamx"yVGsyQ,vt;B;7 #d BoWߛfWXMlwR47zഅ2[m*i/xWsm+]07Rp u <y҈ceAQlN7vkX o.ԲCqQTZ\eY0P_;;w||G Y%d\{oowz֟Wjo# KOT4cY[6U2;e'Jzp^ R*( I7碘Op9vo#l'|'y%o Wǵxݨ/aA+LN&u+0)e{y hX퉲5hLڧ}ʺo.7͐1eփ68"O:lbOdOr4nC5LLmޫ_g &1[Yg* 9ďc/owa!Ya)7reC0*~މU,Se];,mӟMK(ͦp͔j4ldFk3n *I!|ER3}Fв.${ ߛ%T67<(I3UES G*Tt&ey|3lk/@/<>L<*-ʷ&8'JU^^}^xV:+}G*Q'e:_XU s.pq ?~*vR>86]&-r{tTg TDzC⮈]44VOc## ˹Mr.زV.ڤ'G5gyej2Sv"M;ato ztY^;+ X =Vܨnҫ|ˌۖ#%7UGJؚj'b,~Yo@i,oP -Ng]gE+;JSzt +9Vy7J1EpDZϹT8 |CpkdoRld\_4vfrWZ%9S ZzLʦ15*|%Dfx>lFA)5ɮykd`VN Y\׌M7~NO(O(<n7 yMG?U/3ٙiY;GrxEb)݅ibM+ (B<~` S TZJՏGfjzi* bx 1#~)US?3mιZB!7]:c̴6kd/\^J׷a8g2Yκp͑i|H1s[3~!\3ҞݨԸv>ZM cY$%!b V@&&?WPso5 ;rijO?zuF>dg6ڥK⪪2%`qj`T?5P|HBI׹") fr1 >U$kwf?12;rryj0L lJ*g3ҲwXS"6n9`ަm=A߅]kU ُF]qbmǤu߷(p>Ry<#ћ "#-ހx99ÐU'N*chADwXS<[PIM( Jg؜Drr~Q-"%%R!W=dUTFWZh(Hu_=c<&LMLx;>j *b7ʌ }vt CppޭTd7n ĭ;=^Ng3rsʒim橪PoU>ҥY7'r 6MͶlf9 {}9sr,ʾa '~ *(TANtxŕluO 5R DV`<pB"wD56K(jv$n;O7U)cI죤 ֵ7[xL+lq;H0{1ֻzx3DXw"kwr^ێEnP}=zmt&6纁zlaэ9|kY<JAܦykIOBZ%[*Onx'~)qG5Y-MgZ%d)Yj9B n T*tӽ +Z(.՞?0gy Fc)EhbR0qaY:7KRjhGhnMBi.=sM.J ?=7:X(H%Ti(c(' oՓ(1 r no8o"VA {}9slȷ,r`f -ܠ&M-;,D$Laz Ny)Ex> Pԡ8&0ao cF5`Xb %~tj\14;ڌkZSjL?R v8:7 , IqO7)ξb(uɡ 幗3 OF+ɹئ(rLq8gՉ3 sW}Y9}š>iҲTS#Tp5 CSޟC6#`)*j)3\&=T iTuM|M&gNQTaFnQ*"vΫG/w"Kxo!nyĜ7UtrLwBFt{y o? 7g @[xLf:wLvX%c< \(O&fe~( l76u1Cna= |[enUOXn2Ο\қi)P ,+XB­ZbHZG,W|r*DQhm·(y# iܱk'FZ}QF-3Znmk/ZB_46hWO%;ږaX,1ʼn3+6b=i~Ssȏ.e`+FSEArKXm%`+Cg!s1&M;B,cLs By!dCf9[5 luuu|]t|YYY[5YYYU66ZFXcgggVVVVVV6qXcgV6qXcgV6qXiiedȶ7i;.gXiiiiiiiii-#=%g3_,6jp^&r|AUC/'|AhwnmH+HIi-#8+84|V?Icⴌⴌ3333333333333333333333333Ynò̴ⴌⴌⴌⴌSزd[GQ*D0);6ӳud fi*Gg /mm33333ҳҳҳ33333333333333333ҳ33333ҳ333333337777377777777777777777+H+xmⴍⱷ+xmⱷ+xmⱷ+xmⱷ+xmⱷc{m|U]or{&TAηg'3JY)g*ށAa|-lQuCU6(tsḋJ#E e nlU]q@lJyh;ԏieD{[p$Sw +f 'kD'L*9D ۚ.;5M3 ݚ!!.N¼ЅQp]7b.(nw)0ȏ\v!5EАe6JXX##?'Ȕs=)ұ9p+]qyoYN0iX.s?p}YC,: A]-Sdu#b%YdrkZ6{{ׇ*=ޣ5aNu%Mfh;ǡPC߿6NxCfbM˓9 \U.P{Ìnܷ;l>'վP'5B4R6 8t9xPe@ oqQeڙo73.K!ޣO $+s)W{׆-kS̭=@e{N r@+scE<{-_˕$Sv%c|XYh mN O&F;_Jsl¡_'C;:yT9!e@DŽbd [KZv0T=EU+Ǡ&fW[X7*'t|>߽Ӫa޵XlSUQa?1޵4&Fݿ pnz~oC);1 "a[OSaÅsܛrBۼc97 -vnb~1e "߇?e͠R!CN&7^vN11?PuG 0D;am0 oPuJCj.#ޫ0hZƏakcn?up~'js4xGj> .O,T2ֱ!w* i,urUfީK?p5\N!ܼY=SBL'qO woMУɕ?nqކFcÜw/xDDo0^ )mǴ*<& e7 (U\3/US]HRT>].Gr~OvN߂'O$g?:50g(j!LJ:LoMӨ* ߂V8d |%Xo ڏW|E'Tv}M{^14~dr9Fh]vs絕º amA]9޲Xo7<52ES#y7ΙKN9QSnuEKnmn:Y> l+:Ts5A&y փ*?)Zܶ? oݜg uw,)e?LP|}cJ?6B:]DTp 63xxچkUӝar_}I4U3JAM!x߳~AY-E)o>6Q7 R;{-7x=sM;qlږqN:N_>qZJ9І )]dܥ0zLc^ߕ2-7!s^}WWI3nJқ`3(8(榫;|cTPRhMeO$0Fã>ҷOvu+A6L@p?vs79lj{,Bj GYb ANM5BO cUEpVO47'X҂ͱ&N7+Oõ=O ,p(t4FeoWS:7!ih*ӟ )6iA;ZbiEw,#HhTi\9#vu]n)}-#flQ N2f ߿ޝwݰ ~*!|fyA>1зJnlpLc1⿛w9ޢH3b7ubo:=0/3 [ndܑ<':!2N=UtDE1]?*IxŦ蚺7h#gQةtn9,O$ßܴEj:ɔbޢty|'*7SzVc)d|qvs?ME%Mȿe~2mU stpF64s𺯊i(XMO%!زVPi{)}9et{Z;sm#y*!x ,H|S(Ls$pbm5i%S}k|1ӜA_j)&QmsM鲍;#b v,w,N#hݎ5P ߣ%d(&e 7 YY[m*Fuv 3߸t Բ}jIe7mBlL?]ɆH%!d*/Ԩ/QflA:#yw& DQX>TbN|QP_YTnXmWWLu,ɿX1p?_O]d&q6hn4oЃ9ݫ$R]T F5-*'E+pM/ZLRzJLRGc#ipՖ93<|Eж*Jsoب%T6:gct!&mвd`gӖݠ#{ҟ9vXn)>Ity[ڦX7s.aY*oS;AM ۾{Cld|kw>jY \~]k~J&X}~ݛ$vA(H޴,dxVX5a+&H!/YTmQdf1!᳊5B[G \ K(vuI cj7[ډ۞'K-|8v5F.+A@H 2 _8f9lmGF6TޣܡmN-iZbYan2Ɏ:S_y$NKOQqaNjz,l-+H91oʢ׌:9<߂}+KEN#SŎE7@6 6cm8߇垖s b.95U2"׋Aq_w8SdbޛTTNmwstzG.+\>Ԓ:Fd#TEtM'n{x{7E*] n+HJc_hY-SN`;ޝww#NKc i%7MILyE#{M=Ter-Wwi;5TDY[;FR5 鲽QseC]5-e YwC!\歘^*>U?#* b~^R\C) őxb •~`G+Uk1y݁RK<={Wտ.7Y#d%"hrIp#ތjf`#4Qgۼ 'rj"BnKǡSTӉ5`e:*ѝpTKal.©kQM 9M%CXg T67w7{)#Nv#)D4km+Gj]6hU裄^3?p'{~8E;~5FH'aT^r'co?7:w7|o#f%"K6?ڞ16Z&L'zmwZd[Ĕ/I$S30)_3Kw"zy v X PS[آI͓QG`(d E+"&cTr`/y4i͖G`- eQf;+`d h4dsv;&Ҳ;Ns۹epM?`0+(~Uav&n[ ;pXmhVC\T@xO,\oVy3X ;C;VkoԢ"X?>(?304s*4lc5|++EPZq7F.؍;Oob!k~)ivʖ;y\|VʨkiY ʭsspJm>:ެ[<7-1b[=c+u/>4D{<ʵثʙ3Qվ YM\#[?EPu3ck9w:slT`{~*_wS=4y6Z\.9shJk ~2FN ;e n(B#C`*bv^eE `h啕IDxeSydz6M{[Ъre27Ԛ\N=_"SaEui#(!&4go9 о$gJY wZwT ,7׎Dvx|y[i)s.CX]:1(Ν,z! b;UUIIQL!R6G%wfv.ۚ~D~`P3¢0b\b_jc8~>;lṫ$*ӃnV;`xAvڜw<`qQBDr(8tnm>#[ښw3t-x iOԲmK'I#]=݌ ?&鮹⋙ ͹zazTm+GҮ J1= |0ӊa;Oe@L;cGIfbG#L6(),n=EoM ک?߂Q w*SPޕ q00uه~'uʥ-70[]e0uUT,o(f`]+(kگhO?[Hx}]eIgՔoBELXmRH'2{La̚"ٜ.,&܈Ymrs04mҞ0x4ޘLo 2iɰS cFEDFXo?rnC6rmU}&CN-V됝DEDmDw5jtjzjao B 4WBB[r MOL3oR;INBѶֲlmnȶv̶q^"Ktl8 v:U V- Uvcp.7O'Ƴ $0iSiSXB&Xq swrTIX`niXa nSـ[>Q5 w*v:lRTbX0x1 (Xwp4V_mPcf!>h mUptHVG- Pէ  iLŞ)]*E@f'8rg$XJAmSTN*89`r0rlVɔUQ\DblQ )./aFРCîvT3 -#)Fɻrn irGK+!n9 +L ez]I@*)Y3qn?!bA! 8MW(=T43V!8z*һOS SqMqlYc' VTeJW`Pҩ!n8]py++~JX~mr 4Z.oC޴3zw އýhf>C7Zz|;ֆoC޴3zw އýhf>C7Zz|;ֆoC޴3w*c1wibPZ ޴޴՞՞=A?=A?=A?=A?=A?=A?==TYbu;=裌;ւV{;ւV{;ւV{;ւ՞gjwG=AQgz=YZG?޵zVz޵zVjOz=Yz=՘OP?ZG?޴wG=AQgz=Yz=AQ^՟ZV}YgW;=mU9=H"u;ւ՞G?޵zVjOz=Yg{ւ՟Z Vh*=Yg{ւ՟Z Vh*=Yg{ւ՟Z V{;ւ՟ZG=AQG?޴OzTz=AQG?޴OzTz=AQG=mZ VV(?IE?{;֊@w ýhf>E7Z)zM|;֊oC޴Szwއýh>E7Z)zM|;֊oC޴Szwއýh>E7Z)zM|;֊oC޴Szwއýh>E/Z)}zK|;֎_C޴Rwýh~E/Z9}z;֎_G޴r?wýh~G'Z9=OGrz?-h~F̛֯RBY8ivN2K~):Vҵ>2?j+TZJ:V ֩UZJպVҵntW{sJtJVj+WB_h:Vh8"_rt - tJtJ1[zhĝtbX` - .}ȶi\7uu;n$dҭ/B-/B-*,kƴƱkı,Kı,kƱ,Kı,jı,K&٬+ °+JJJJtJtJtJJJJtJtJtJJJJJJJJtJtJtJJtJtJtJtJtJtѭJJ1,cX֑cW7'5ޅPDZJܷr.ss;QCrts:lA '+]4t'_a,'7s6mM<\ 5<.~ $v4 4Uso-Z VlC[o!o_ɞA]g1ySs@7WWߴ<1)ӽp;5t#FA|RA\ 軂w2!w-rx:ue>a6a4 ]oD'TgzX#:D7ܩ*&C+I6tQNݺAa6@Y3nҋ[b46q!D70b& 0ji$I#Ќ"[YY9glkMտ&sY[W$8sȔs_69NOTSY] g&".ݘ+HJ+"aMje,p.T;&7۷wVI &A$}qQg:?^&\{P4TLqY'A9UR46yHYT~U앬;n/\T^=O6cÿ?4<"?0|G1tyGQNNE0ϽYNX' pڟfaQ$ 9vkk[5@mCI WLkO {-mw;߻)}<)}C m; 2jQ;~a:-+78}nEh*MI_Knb;r7=j T.3Rnp#ݻV[_Q~ soNns@;4ILZ7| Rvtɥֶ^BUUcS[g}TB{Y[<"\s* )^نh_oWurP`}[z;׵dNR?zxJLQxTNdOۜ de|;]_$/{Ŭ1&ʋ($(Pw~'e6WS8T9){rh.nPކkeuup9uuG*S\pC36.rhN*U@pI㳵QC]#tQ6# ۔1:ga}@S|ws* y XaAwFݥEP -\,#>X9܊*<-#.@"!?#mn^eWW&iw]_.RI&a0YQٱbX)ꎶl6/TUB&YWV>&?W}_h eǿ$N)bk*Se2eWo>d]c6\r\Hi#;ڮE?tY`|9e~Vcn<}T",ooǒNUƠo0iSU?UT9CZ`8 -n]ճ[[GU4stQE_[ ا!1ݵ]]unE nDbAMeFgFX,amXҰ  #WjX9s!үd.ګ`jGXY,a7z%b{2f:.]}:Il7pYդyQXgI"ݻG mUAcz°x;cY6: -4tUmQn[D$( 9(XvvnA9lGr6 jS-d|Jh͓ yvvnVDlaeJ[,\=(tdfnvlsٵ74eQUN5~#nYK((- e,864~ Cb#rxR?j&OY9'vw_Lwܩ6e,̀byU9Fgnbl_[Qn*>1;'T fޡsp AьgjVLL9iQxiRe ZOj)&<~@yg?<>@Ⱦ{+rQl"@ےl2zmkZVkTmj,_LlV_jd鲱O)D fӗOVAi_do`G~bG \`ųYk*䱠4󍃧;*+O(:1蟮d q7YdY%,iؤ1[d񆘿%O+%kfz* ܎eX1Tn~pPATcpYB fB1#ӽ٫P)SŎz:@p@ _]__1y|'yG1U[ljͩcWVNk}ʤ{ "֔}#ad(;a;&X)\ G5GfK'.t2>slF\]P/`ftg-bLubLubLubLu͚s-;rNxs'ܼ;oaQe3. up?"HhQ#uӞ=`MH6ߕ%DQl{9|_#v;j$S<9$*ܛ 폝?-lLˬ#{U<쩌J3cbLS97*S=; )6 SJh>0;_VW4k%/]oZ"ZvޅI5#7nD&FL%i4PVLYKI3#oIdhv iTR|\Zt(|ߏz y*"{O-vZ?=%l)kbpռ8tȚv`yT29q||Gv>4,۝  |ۑo$9e9Y*+# -+?K=#ʞXPV@3Vϔ"eDF7`t/o9ۂlcu.Y.Rãon[Ӊyj>;KO:cw&͈ly’BJVǽKK>6™W_oW<Ϻ>*xvҳ FnY:84ϊL&&:19soAnFFMޠs\qnh}#И_EoNް!AI$*Jf@5S5m8-ەemÏH%7h&5kNۏb:NdVj.,,|7)Z78)^'Eڰr,O,#=;_|+nlᝑ>Cf r<ON'd?ŗzll\VO>IO=_)zتlݾAXwǦn [dzb.L]>`$ձ1cĬbO:1rݿB{Jb 8݉56T=4f 'E ΙM$$C*&9ouQU%M*zQ>m?T/ >78FKN@XTdoάqõ99ć/MA˷&SMG!؂ e٣ %M! *zZz*I yde''ϓ^#4ϔ7 |f6M5$dQW#ZUM;7ftܚ"ʛO-f*Yh[|B0Íby0"[GBϚnX?{#plYW"i1.Ҳ6Em0<!9slȷ,g!iNchjc7f&F` lAnE@i:dXAܬ+n' Iёrl+yS}hXn۶|ѤDT:oyIi+MN٭ae,bV*([T6Qg-DzJ*vEo%3$7܅rdmY>BȲ䭚٬nMG7O;)o0bMO_S&Iq=Ii2{ PM̜זMEK)how^{s Z=cziqEA2|AB-#aVVVYY[5z|S&KqدtV'e[(&Vʵ=D0ďZ_j؎v.6_b2Os27 y'uuuuuuuuuuuuuuuuuuuuuuuuuuuu|WWWWWWWWWWWWWWWWWWuuuuuuu|U5tUeeelVVeeeelVVVVVVVVVXVeeYYYYYaVVXVeeeeeeeeeeeeeeeeeeedӅ @n*G܋+rlJı,Jı,JĮ+KĮ+KĮ+KĮ+JĮkO !1Q "ARa02Sq#3@B4bCP`cr$5p%s?沲ڂ%8\Y9ڬfM IDC HjO2Ԃ V`LjS" blѽ9@u.5& .u#byhN6;AO j֞@m l 5*ڵZPeӅ`KcBjkfSmYLn-h˃SSmΉWM9m1bKv^k_1 ĝw&py{ށLŒR0du !i^tNd&pv$OfmZ"نv:vy+ZUDu~:xlLJAź|E$B7+33%Ne&Ki %QH\v5QFێ5oC$8buvNsvJH/"ߺt2`hI;`2<0sdZj$w&j=bnG or(__6Ϋr\n4j؆BÉm z|8 >h蛇uWfD5jr%CF5ȵ.v~srvDfC-l>>j99*ƵlڏM<~Rڰ[q5gвrO<.( 9O$+f􅓯#~tsBNGUa3sRɏmT>|o*3G:(}]9|v;U3{Z?Tɉػ7r?`UmNoM-c;[$v@9KF pPM৩ъQrAwiTHKg߹T蟭It3_Ht,uwU : {Gw=:sb5A'~RJd6(#vj?T9ϓSZ?*ֆ݅L!y]LO(*=Sc¥O)o'1g6yɤJ\.x-PN,1~e>SnG cJcShFW)o Uӵ"lS5 ԍCQ؂#R(+obzȟMCIwV;.cew=oZ)=pgl2r%8I ~&Ӌib.?ڤ&=/wlYNE&Ͽ ~*̂'IŽ줂Ϲ<9T[nRI϶ҲcVM Z b+'|o{? iqŭWQULm ~tw5}uӸG,*E<zvvAGoӫ봪yc''W#Ԫ,⩈sz4~B^P_sj€! h))vjAsf zI @ĝ鐊{sj'Fڽɕ5^Q|%uLjC(4X?༧WVNǙeyR<|rW J Zp{ݬ!?z J{Z~O)WHy!5%Vn8X!ta_RkCRw(sf(P A衜 kA+oCj$7js|fje]j@SC&Ibooq(-7 OB!`IJ95WWRCEsJwE[0[sJnh+A%be7 ѿ8+o@ĺV2r] %([3P8lS`s 7% '1Mr#va!wR98XXM.?SE.)OHHCQࠂl!!ĺ%;X=h-qN8l~|[6w-k3X:27 P6Nip38(| hVM WUi+]n}jTfr>O"w_v'hpIly_ bT79*8N߯jC-w_?6?,?G qE8tb8w.|,qupfu Ža2T5oQ}#mCfHBKu54Ԯ3lUMfgnL)<ڲFA̻)9v,Y }0 Ի޾Fલ{+Y ɳ-w/k] mG_3 Z Z@΂1>8Y;4mrxjm؜U)9pR =/)Lw" eM@ [\l^Lžy'zyRmC 7Z%\vͪ:FmF_[W C@pj1}Gꪜ[>qK3ҵA,Ѷꕆ*x"4>6#QQB݂1r6o?"J%P+)N6QĤ9|mOdJ%dl̕'!uԬ|mXVE'1_i2+ansT:;>{5EFGe<5p9_gt`u18pG>3`f!E>Ny}b5(fR ,qi|7&|Sbkr-1I̙ pԏocU,e~о#BvB(ŪE[Nb1Ù[?Zͩ<ȀdDH6j*Bj|,m@]-P#I#̩ߧidQYR.ؽȅN2Sk~e2m&Vt_'5Nx$nB.; 1 `;TWVj K }3lll}Rϳ%=݇b5%u4f4zdn}ՑJ4{ϿB`q(߭Cj6q~U4GTxUUM}JgݸPٜdY6Ҷ3ۛ/U)۰*8YK js5Ͽ$TuCQΫi$feE0l,饟M o}jYqc,Qy]![OȮSO(tqVT.G#ᵝ'/"ѝ^C/"QZz 4?W MCN٢7a݋9T]T;S}jPQ3b?'&bٽiB q8G~NJ.hLFi[`L`_Ee:MȱٚbTe/AZ+IFV_T9KʩZv31V^h(Rn}La`@*& i 81DOLRZLân aXPnډJԲCB:EH3)ة#-A[GA3۩8 7, hSG雡"GsgōШpi 4ZJuB&<'G3bOA\n\n\n\nNF g(?+șC2Q?w.AP0$ZXVV8h:|2E 탌֗jh\nsHqB|ovJW*']}2g?g_M;%1Ig#c]I,bD;f'5 ڗszFߥ'ٜ~ñ^XUUߴ$d-2?`YFT:c@kPXТý6B6i҇ '4NoxQe/aG3I5#`z&S`13CO?\2bָ@.GMnȎ}820!jo!a**$HäZjsG(TX==][#UNeu5D {/vɢԭ@M! %A:Bn ىQ3T MO9?ʿd8MlQo!H-0Ҫ"0L1mLV ev8aq.6 8nRFlD1FNc^0&De]L7J%-.퉡0Yb(r_Tƒ=uV EOAnT\X:VGqG;!{4 /E>'ǭÍ G؎+tb4pMhҞbcaľ{񯜢+ILR2eg%cYU,R1SΉs=}` _bݥi1zM ݵS7Fu\3Cֈ֊6D1FcxՔhdۖ]XPjGT5VاT_\\&>OZ[MCWUiԜ#w?׵:ӴS,~N41n6Mm?0M)c&O͙ȸYb*yhɎJn&HSMAY9쪿w_4;*F7 PSLt r9Z#a?'VFBc1 ; ƨXIsrsUlfOFEm lT̎KQ1K0)ʥbmN G3&F̯t$KuM&.C#4o-͓L]L$3Z*ǩ[~zah^#؅vŧ? V@$%5c,iyL o s'm(uZ[4 NT? jsM—b)g0k+ؽdxps`rB?bʍm]SQdM s J ,G9a⸧,Mi1[_Gy#мT00 QR+I}:ɡִph֪KC?@C{J(q/#dkL l㶡ibl-hS&?|p]XLvra=#y͗4/#z ޅB?PӺߊ w-GaZhí1wofmUm ~K28 #z? ޅCOZYuzIiylMM]ʮ !xc6C%y#Ш+Kq_W켓_g~( ;~ev8Uj9lN|bɷ.,t/~BOy'м^J* *:M,40baOҺC5@. A(9T=Ty8vEɰ _ޅ_y/^K/%x4:`ç켙^L(~c}Jvo{LP9VMCy~0? pZ~ ɿh3w1őF'8RQۋlqB݁ 8\pDpG}z!1^ѡaGIر-.\ ߩ> #@OL-{&R`8}1جpÅߘf1Ҹ1Z7 }#3ձ9H߻UNñr\sdWTd%Z^ZNZ^Nx++]6QʚHZ}LզytK5ՑԂ+ 9}}{Ц%fXSSGl> hI+4mmT=LR dF̛j`ZVBb6(_…n7,riўn+lvSUpi#Bvj{̆QRhx0F';SXp7W|cpkv!m#l=#;+ >8\S{Ec+XV2l!PLӧNGp}!X#CO\ ^#joZcXdnqdڐ f$~18;Xu&H樥xQdv+')- S#k6q#;iM`fRF_{e&V}@dAC#NMə=J??_JԣH05_1]%QvZn`[wfs1&HnЩcuPV '6)-#zo_g:M( v,ز|RsGQ J$T3{>97#$b\- իo*ܓ<:fw9r7Tdflxo}QJRO QU<1}{yFr-K͚G׹yh {-Cy}:\)e ڲ;^p~<8faǷzDz`~@y懙`q 9VMnU+JѴa]*,JFRE7;wzo?UD5ogb?7,TʕHN5ٷ߽V85\)@c֣,k5CV)mqfC%LE/W*7D_˖ت^cS[sݱ=Ȏn}c5 3[dn0C0-M% q((u.n~I Yz;Py'Rl!e̮$}P#e5ŧRWK'ryNQWK pڗ:_5THnjOxrXR}Dn:#|{#jm|5!UKv7kp0I|P[ڼT?_MS4抶x[ԼS{)+gHe ,^Qdy;YRVI9yJ~_Ox('mG)՟K^TܥVф?'T4ܥJpTSK]Tv*d,y}9.?~Df +p*"a7A D{ 5ΜpjGYVWbltMEZ٫k! iQF<7!\ eb'fuYl EۙUM4bK+u~kR(0ݦI&=o3n85c8?!ıc <-u`umWX O fڝg{+@ց`@(FKs]^uu=Z`:%u Ũ5FK *R?kyfv|!m! k@]m6 éӛcp/t3L1Y޹ڮ09j)5flEQ;ֶ"miluR ss9Ǟg~J`# C͌ WEEXstHQn kW%ZQ'0P-<+(ɹmAY;JN6_~|}G{__5Ϳ 3 [%(3ێvdۨk)|ԌMbV)vsxLxiwD~ ,ڙ+CKy\ۓYXfsڹ2_]FښD7ԲٷdhTp26dOEi݁~QcmRbEYJgc#[*T!lc+,Rj*p Q5:HH$1sͪϭWSYn͜ʊnNWɒ6.}za@ĆIͣ ,HOE)\o(yyvaf`qJ(<\B̡9dP_Zh9_)(}Üj4}k٭eܒ̛1Jɧ</_~!?bJMNE'%>D򢓄b RcCɒZ[32Bc(/=NaֽqDhG*B5XlݭdHipp_ͳM~]0a_h .Q:{|B~ϒڰC |VS8LЩG.u+-eV$-%d]Q=ce8'5PК[61z̞ʁav6<8C0:8SRcH/uXwSXwިvpjoT;]=7\xMkVD&L-xUS\f:3qDZ0,+ a ؾ=#oVC+zD0ZQq\as+,.i ^/{. MD5upzoT;]v9c̃ł|L,{>U`J yv2iAWOn'/ˎ[TT٥57\x8iPXnR:3USq7TEUγ)i]5dNyJ9 }[C+Sz<âZl͈bTp9N7O\ՎxMkWc+SvpzoV;]=?\ՎxOkWc+XwvpzV;]=?\ՎxOkWc+XwvpzV;]=?pzV;OvX\ՎX\ՎX\ՎX\ՎcpzV;WcpzV;VՎcpzV;W|VWZ=_h |VWZ=_h ||Vh ||Vh ||Vh ||Vh ||Vh ||Vh ||Vh!||Vh!||Vha||Vha||Vha||Vha||Vh!||Vha||Vha||Vha||Vha||Vha||Vhb|V.hb|V.hb|V.hb|V.hb|V.hb|V.hb|V.h|V.h|V.I Iha+J*s6D)LQaD+ ).~ Y̡Hrqt AI Z ^ ?[-bā֠-v~c í\nofݫ(8mاFb荩5K0'R cpPˤ9,NuMXa~%dDSrt$S18dQD!FlճɅPD= w AR6wf@Lyv1y6S2uSС14,ܰ;rȴ?kv(ڱ;uOG!* LYq=^1p p~-i|n0'.h?sJE7&4N>A!*YZysYe|ۉmCjk+)& pL;fK)ed 2,g1XQ+'^Й.^/%S$g.> ,QGɩuUcReF}~覭p:UR1O9sYAG'ѿw?jËBg2{DUyXqv,u0UڍdՈj ʣXo*:&wŌ.$lv/Cc5?x9T L]@T" )i_ariݽpZwoZ[HtĤSfgy#5֯mZa{G86=7qtcΝU+WI$eDgVV$4\l>ayҩs䝎M֞,SWY$2`bۜfrzħ^ݪ #u iX˽ک9)Jf616ض[uQ pަh Vݩ#qEY}PruC})>Tзӈ}+K1;(:Y- us*q>_\DM`T0ݢޏ־34lJ!hB8۱Ucx6E"UMáNrp*,lUJ i}nG+@^r'$L{f$Ew6R\dܢ%[2YTo4ΰ©ڢc NaR9Kcc0ɔU?b,_ -E rN?blә-`#dsd?wI@eH\"K?GQeEHrUwZU`֦W2#9i^=K,' [>H>GN%6ạ֜#>IysIR:26%RRȺV5=Fo2ⵔr5MfKf D,ݨ%;NrXB~D؂dܥ%m1kT3Sjf)'e,8!9χ,V-{wh֥a?w`Wډ(9pYv}{l8.zЧC6v mDdmed.6&Hl {maB׷ c Eftʋ^K}%e -E#ˆ!P57qLF ˯JҕZeߍܲ/e]δ],@_g4ڍ"tǩI oF1:7mR:Bqyz9'֚0h8YpW,94`G^h76g;sO}<"ii 9crx&B NLN:B|͓9=^XuGN\ {W%oL8v+lڋ &CbLܤڜk d~infN7^8aM7HSX9Tlϐ :MM%92肆F2{i\(4#G`kcJ~fS44M|ݡS6꡼A9NlSXyz[a!y溾{?aQeYZ0UVXAXBa ٚ/Il[UWW*lJfqD{-!ZBpKM@Np=%[ xɔnnnO@;Sl݉vnC촋HźV"x؊x5ǖ^å܁N\>x To\:xoc|;}F7.Qv7p㱾 o\>xo[}FV.Qv7p㱾 o\>xoc|;}Fê7.Qv7p㱾 o\>xoc|;}FV.Qv7p\>x+|?wp\>x+|?7.Q TooFV.Q Tooc|;}F7.Qv7p㱾 o\>xo TsV&e p\:x&LvफS[uFê7.Qv7p㱾 To\:xoc|;uF<;`\:x Topx.Qvê7uF<;`\:x Topx.Qvê7uF<;`\:x Topx.Qvj7mF< ;`\6x opڍx.Qvç;`\:}x.><x Ovç<p\6x ϼvg;p'; Ͽx.>l Ox.>d Ox.>\o Ͽx.>\s Ϳx.6\so Ϳx.6\so Ϳx.6Tso Ϳx.6Tso Ϳx.6TSo M.6TSo K.6TR K..LR ˿..L2 ˿..L2 ˿..L2 ˿..L2 ˿..D K..D|x沲ڂ%8\Y9ڬfM IDC HjO2Ԃ V`LjS" blѽ9@u.5& .u#byhN6;AO j֞@m l 5*ڵZPeӅ`KcBjkfSmYLn-h˃SSmΉWM9m1bKv^k_1 ĝw&{ !sp^tNd&pv7^M SF븩 pA$&IF.d vPC_=ֲNai Fڑ5-gckiG@Y[<|?"Cq#"= <d6U0ý0=JeUTBӂVŵhZѵY8.e_>y .7'dXx*ALbl!?$C M{>[tb]0XCȴ]ss7ZZ-Oe]@܎w&MYt,܁@S$KNS ٽ!dߝ50Ƨ7hq{p/"&p6j̠*Kѫ:YcSMT5{'WnT{M| hRSG*H#n=MKMOYHF* Yorq(Q~zii~Mv:'ܫH w~œ0IT Ԫd,ggrc}Cަ<0__7gzvʖE +O# [hظ#& cGxLt^yˮu{-!Ŭٿj*iqэW벊jssoTM[w%%21E,p[׫hp|Q7uUànG cJcShFW)o Uӵ"lS5 ԍCQ؂#R(+ЫZ~?*qnKE )l':튳 ׼؝}]@_TÑlfZh)ݥ}{R'~ ɱJd੾ h|kpaYE!LQݫ(|ܪDø_.UóU NsU;߷lB#:?e9~ƞnOϽPfQ*4oH۸S[i--N$0|@Bq:QٱZ7eRhqܛHmw `GxMC~)d} <5`M٩͘+' (qVL#wB(O0VނdkUFQe2{3Ôs$/h/Բ>]~?>%RW&ȴ ׿7"T0CztpV*mNC#]9QY?#ʑwy+F4 \vB(ŪE[Nb1Ù[?Zͩ<Ȁ(c۷HphYXV4!QYvN 4Cl^dB ҙH!b{Ѫ,D&J6I4W9*HF݈9n&%tmumS XEU޾bz[[MSHRF-,46 ')C9EQGme]3[H+J&ɏ;J^y+$Q FȹMiXTFHzK#*HKE%añQԮ86g=STN3Mqo~ʛ%G2SU3KI|ZLEd8bneU#fѭeU9f{|~*W8?3eBpC_<>妝r٪NPQOg4ҶG8(<]m _rb`;i+`@d&؅9F&b)CYSE#ƽHn V20d5/bdkO8;3iKݜ鼾QVS;ZNTS|V@jX]ZnLlJkolF=_؟X]UM=㛽UT0 nh ؃4s.,<\M͖#Ǵ?Ii6aߩSbS<t5,N):c * }cm6Seٍ=vI\,VS1Xε2nFuOriF֐oXqݨ>u;gr ވڹ+Qŵ[™pwSn4m0_=U2@(Ҵ^\9kFURئQ6+PG; bX\tWyS_;p4AU!M.sEʨL$'U|bjzS~sC/"eW&Xy8PW Y0/*7?+)2QUԤ6H$bX˝#tκ6}lrH ک[ Gnf]uv¸w.0.Mn;:1p>(lSR!G*g$na(OgA9{VO j#e1w*QϨu)k%GH,}˒*\+oFǝ9eoD&92Rդk9 -V"70NrEdu °I֧Gnc' aj|NAN#裃;UIzfN#(i (CMzmhHW֣#k*4&RTI&AQا'4]b#RRVpaּ4 *khE/"e 䛮9p$aU?¿iz-Y=?g45VUC\Tp:@/dkGEB |6gvIΛڔJ6]ڢ{؜ *v*fXb>a3gA[Z3D2`ŋ⨩ HS殠`o*'^{5 >w6{WZ{T7$'}KQ؛`ybϚ9(5ØFwӭq֐)ZRJ/'͖h[{cv';zl!YY4ҋPod$!iZr|d-BV- Z@=T:E쪫UɏnL7(G)3rAT˧9tgKeڞ> ɵ]|jz'(2Tu,`;݊F7/bdu U6qn"pn+*6hnW&)mïb+ X'lm[b!]mͱb@f&6ODX/T)Yw]3'Jt5֬]jVVA #j}+[MLajriꦙkWUlDz~+\:á\:á\:á\:á\:á\:á\:á\:á\:á\:á\:á\:áP;WpWpWpWpWpWpWpWpTkNؙOܵ-vá\:a\6â\:kB.t\\t!pWpWpWpWpW pW pWpWpW pW pWpWpTyI;6TmCHW pWpW CNQq_ʍv) ᰮ ᰮᰮ ᰮ ᰮ ᰮ ᰮ ᰮᰮᐮ.........q.q.q.q.q.q.q.q.qQ.Q.Q.qQQQQQQQQQ11QQo٭jKD]kFkFkF]hƜ"uͩ k Žl8ڱ,}H=]bW e+h:hMu +#d s[اݡ6l\„EI~ddju ,bRce{.23^˅ u.a ;Z,/I_+O8[UէZ~:Z~Z~Z~Z~Z~Z~Z~Z~Z~Z~Z~Z~Z~Z~Z~Z~ZnZnZ^Z^ZnZ^Z^ZnZnZ~Z~ZnZ~ZnZnZ~Z~Z~Z~ZnZ^ZNZNZNZNZNZNZNZ^Z^ZNZEZEZEZEcX5ZEZEii5cX5cX5cXְ-ѭѭZu VAQR Qڶ 3<ġ֮@CbA8H4RV ^فnPlζamM CSyI`6w?CPOv0XQ~O )hR[ Liʮm Z@[EĿvVV#TLoU`1 +,oL18H%˛r>J \lv~ɴI8kjPk[-_>I|9u54S "ܧc*\xXT̎!hRwJTq}X X={5*JxZ׶1cnaOMRic] 4 ujp=T:JkuOfٷȌ#C6%pEY@QL&(!H Oafӎ H7*QMY?[5s-d!8M j(dž+DԎrzR?g)LkɹLK#Zcܫ6* pyB .ýOҵr?Ы9T̶߹Pt=m]{v~}H7[Z sWz2)߄:۝(rtPʭdL:fYD}ױڻAGSt׿&D%hT&agwKq~McǷʋWg-ePIx ~5ޕH=)n]ޚ5C#NxHpRhF۟ň2+ѬK GRRE3p=\QN-9VV y2sP4{_%YhDƋYK)Iow28[؊*zhm*&CH#ueSMLz9]$`ZߓqhyB`q(9ZíOtmYyԘ0KQVآyՑ_i,-1RkE8²°fѪڪ!}4Ms\D[XT5:jZ/.~Oe=e= /ce^7Sbۭi&gES~EO_FSq0n#TCRӉez\!bq`sm\W!Zbܸ}7I1n-DYƷ|<ݼ0? 85EQWe{hDV;P(r]^e^QiNק5aO a6PiB 8Zip/ʶ9u Q u`LihJSIFEM.V#uZѽU|:X(sRJv+5oTodNY^Y!nc,P _DGQb&牐^f$4br!3ejhiD{¢ʦB#jխeSKRh;ln0ų0s8QE-e±jZ [qM=i^ZԁGub QZVtNx%Kud6q _K) qB8*,!h4ng}sB%nʺY $OIͨx^Si#iRS2Cr]mԵzvTJ ɇsyO+$SzP5yY`Y:iNR ZF:(RhaY*\tѶH'A#EłZ@(8i39O]<ό C<OִQVh5hZ6,\4Ւ̫TY>{*K'݄Y]U'(~1Nt3ˋS(cV8ČVCWQE|dF' + -FqqVbJ GZjK ,.cj Pk{Z @5*9ԾGPz6*,H8Ntt Qu:I'9AqTwW#O:h U?h~Xfk(shw,"t+K'H,"t+'9ϐdv'b6eeaAbsltKr)ȧɄZBKcCdD,>tI*w=s-䴤' X۴,ׇd9S*5S>f'ZfSu,ZgbIN{NDLpeVNZy7߉WYf {-[}l*z7zQ.=S3J~]ds 8ߵ٦F5-3,"tk*HP JmRYv+̙GXWbt+K'H,"tZY:Eid-,"t+K'H,"t+K'H,"tZY:KK'Iid-,%tNZY:KK'Iid-+KJҿ-+KJҿ-+KJ?-#KH?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?z?znޱznޱzqciG7iw,B:_GWFS^t̛-9/\nQ 6y^ks#Y5CT2W5_6g:Ͼy M-fU/B,fRǭDSa" HA N+Mq|{X.Kw[j%cGG͗,hH<ᬑ(a}lE&>v=¾gz((8G3vԅ;A֫:Oت$kk&<9tPY; kjH\g/kv9sL}4_dJ#]N'bC\$‰up jݟ0Ѹ!MOnKRд|[>}miplTL=Trԛ $ǺLo '}7 qXs EQRI/YKn* ;SU'7dc`ا'}nOҟr< O#g'bS\IH&X4\!AFN`~R՚i Or]ҷԠq4⮎ܧIj$6jvP}qjjqgP8\ XPN(5l??|_=Uq{$ e6au "XU4Z1l)jMԣn%8W<{Tn-r#T1M݉8&RKbjv.Qk]+٬R Gh֛!HzŠI&U 5෱ lvw(K)oqDިr{Xqcܗl[bSx6FaE $2{8=NCylML=M T9Y#{ +&bN:G_Tjse?δ'$lqxڙCR )FSTNQb6B=I>jZg[2;yR|/T7;#Άjhɺ@Kdhth\˃5aCPx9>Y^K?TA'[$oMͮ@b7bwafaoĺ{Tb;kjHThQ=:lm+X ^KJwRcɏAIepdNrcb,Tʲ<>kA#MM\vv&H#mCGj1 /(T8v0ٚ7aQL;pKMs˶AV՘c6UCj约?tYP 6Ԣ\겤r?\soa+LQ;;#*wi=Y=اPz)v}Z[2KNIFwHyjhwGHF%;dm,xqe*_*)& 'Hpe;fo)XҷqSw{'B@N6,'Pǫ[SsmZMڎbnA 5_dU{ԜSCu)J7WL^P޼QyF}3^PzzoOyy ufBPƴʝ*&r.؍#[7hEH.^FwPdqk\֢SZ0 hhj70Z豨89hO(Jhw-b Eظkϑg9M.´oS}B#Mu[H?D>¼A> ,Ѿ$n(C,~h/Os/* C]|ӛ"-kR6::](\p Ψn6Gp8 1f :_"pݺb6gb0DN؛ز4tv/Oe'w_\zJ;}Z\vNQ # .I}qmH2xU:t|5!CTT7]n&F-Tv|"|`fxYрCێ0A85(Ƨ`R@kDئj("A=sg9 F>lO$,/v;du(1|a׿Vꡇy{,/T}Ni'7mS\da{G;]*j@?aAJGoT-#U72`HO8:WA3^f:ӆG0XJg$auqXC5MIdoU=w!QTԔ5:V{T*Js#Y0=Փ'\6qs,SXnUy&76N~&O?gg9㤨'ܝ.]SC? J(<P:E]:A2E**.wLt< +H}V.b;Qo/҉F~bn$7(;V1̴rݨ/fE]hã'JJ1Dg7UT섆iPGU;7~ʩO*vշO;lEt"HMۻo?"J%P+)N6QĤ9|g~˾UzI1-BR%m Է5>AUd+E\#Wj1:o榅к HX 50m*'SdtTT!%9F Suɑgpm؝I&IΊWJڏoVz!o(lZd]e#l9_frLU>E, gOùHĴ+6Վv9;ŕP0;PIekLnԑ݄)-TLIJ1]=eG]i$ sK$p@H }Wn7 ’6n, ʢ*Vo_jr1p=-¦R} Vh[ĂˬT>@,q9F}֠{jaZ$ahS7GhF/L : gsتo#T8XG7W4Pr(Rg%R:T@:AudR),o7,\X .UGwݎrTdЀkI##ڌv!kN / \cQT|\۲!W54In`⣐6g=STN3Mqo~ʛ%G2SU3KI|ZLEd8bneU#fѭeU9f{|~*VqS'e^@rT\;OK>q<> c&*xՖ=5 /:ڶ_vdZ0v\`CϔQE$Tu3d |G iWܤZJ9&UIl,6 ISSP0K\y9>S& &nt$&ݥYVUHAP$ےܙmg:=Y)sqg?zo/U"S"$:0mq֛0ҸZÛQ`88W6'>USutxUU ,p6 5y wY2)qSnIqgEhlQUї ]P6=jZ[-$ C*֍Dl"T 'MF<{cPQAR6Eɍ7o> ;s*v͇A_ئj۩GMvi}eX)#kkiMa2K e<\eIA%D-mAѯ :}Ԭ&rOWՔO!9P:& FSI/n} ~' C?ctQTӍ[Os{uu,EAP܃̏s\SgY8^x# k yVPJ|N˯TNnudXV5# kϒvP9u;_&yRhĊb䪜41ީVhk)rMLnZNH7ߞI4Gx Tnp ôx>p3|W7'Tn^O;[⼟Qvy:po}Fõ+k|W7'n^O;G}FNJ3+ϻx> NJ3+ϻx> NJ3+Ox>NJS+Ox>NJSn+Mx6ۻNJSn+Mx6ۻNJsn+ͻx6ۻNJsn+ͻx6ۻNJsn+ͻx 6$ۻNJn M. 6$ۻn K. .$ K. ., ˻. ., ˻. ., ˻. ., ˻. .4 K. .4n I#&:sͩI*fU1{e *V5MWDygj6Ng.GKP0z5qܤ?HVN,VXVR;pYNcy2jHlErvH:w:f ־ztRu#j|&jl[ҏ'=lkG:m#Сv>U_~"'nݿ]_ Zؙϱ99kT↘GP }C?_ۚ nՓegAxVOAU$2=lef%>N3_oWIui;&ᷝ[>OS MS>Tz\V*zWVL|M+)HY{+he#I&`y܀-HOZ|oEC1}ȀE &قF\}%F۫Uxp؞sGd}G#1s%6Po>=q"曓TKv~⤩lWZFX'F%߻IK[x(s5ai }jR? ٴ{~k&X֗aBcH{*btX[wܑڟ!؃TZ#<̤mhmC %ʼnGY+ [gͮP>wj7BuRC*[Ud)#6 pv6\pvT :dVԈ߱?#R7wJPHqKIz O%3d'ڟ[;4i]{fEs} ~ҫ!1ZAc&W5u uy.bFyәn(nf4- ްEN#vD;uL\E ,߈nkG f&Ч%>?.?CHf@7-܄YJ&\t}h"s*F8cZjń,&0U=r[߂l֔05݈*|[F/7o(ճ9K 5\+$t?Am{v0wګ'o߽T<n:ȯ0Jd5ڈئJƗcϭȓ]F]G$l87s!NƛV؃kZy3=򜞉]~P6psʦ"9Lj{ 7fﺩuam[5Eœr]q I9^JA[(~Aəw9z+vIQFd~11(GPb;l}Tgj\{ដvCF15e6Smi1XmB@XF.pٛsIQf*W\35F=͚|9Ghn':I߯)")i&w8\hPenjYXR^)$[Pw,K[hAě(7l@WͳYDwX(ar{lvԡEήAԡ?En#>"y6Qz*eeeen4&ܭ38QJ%mfG{sK' H߭ ^yj&*ҿ>H;t9Rڂh<WSd\nsqb !J,7Su(\fV؂0ʊ$9x0mfp؏VZ\lxmU1]S 0A؆7hrmTAq΅E3%=c>}C)vŭ(*%w)= ݳ)q_̡EYu4WL`_WZo`u0FGFCbGܟ)g3E0̈r~L-#BIC%b}JzGi)^TX_b,C+OctvWv#ħd,?GaǘrTE,$loQF#my$ng9F=QߒĬ֧G}'I&'C&MkX,lT`2aTMNM3G-Fћס?U펚RZp27?pO6"/n(!eÎ5vFveTa NUT:[G:AS +#sMtʰᲨ3DllsfU8\ Ir;xm'P*a%I.ޭnfm❜Fsq`0s(_+c<2Dvux(8u|畱܉#:b8ޣce"~Zow/;ڊ7xQʟjaXsYGwi[TtR]TI}\g09ONN>7KBv`=*Ly-^NrfOhࢃY6̊@~(8zĢA)jGZ!|z7iڒ \%1kBӭ;rѹw LYݹi|WN OOaZzX;-=?i`> OOaZzX;v OOcx-=?ZzX;ࢬ'bl d OOaZzX;S|w-=? OOaZzX;v-=?i`ZzX;*ভswH;|v-<࣫'bl'epc > Oii-<v||YQeF0A|LnaZx=gi||YZx=gi||i|i|i|i|i|i|i|i|i|i|ia|i|i|i|i|i|i|i|ia|.ia|.ib|.i||.ib||.i||.ib||.ib||.ib||.ib||.ib||.ib||.ib||.ib||.ib|V.ib||.ib||>ib|V>ic|V>ic|V.ic|V>ic|V>ic|Q1Μqsh]`Z%Z.Z5Z5B.cNnuu yrqfԆaGQ`pX>+t\4u\&B 9SlSnЛ6.dB["$22i:u1{)d1꽗 /eº Ѕ^M¤~T-d*lj-?RKu-?R-?R-?R-?R-?R-?R-?R-?R-?R-?R-?R-?R-?R-?R-?R-?R-7R-7R-/R-/R-7Ru-/Ru-/Ru-7Ru-7Ru-?Ru-?R-7R-?R-7Ru-7R-?R-?R-?R-?Ru-7R-/R-'Ru-'R-'Ru-'Ru-'R-'Ru-'R-/R-/Ru-'Ru-"u-"-"-"Ʊk-"-"ƴHƱkƱkƱhkXh֍h֍\-Jj W[QNm+s ([l([nPWNy؈ \!^蠜HnP)pl7(j6 LGbh[V}J!ư]H}iӟ!'Z,@j §͈4I-ru4jW6-vM -l_y++y`gQ P(!ĺeUK%AhnrTG׹ I .e~F v'dv5UJZ>ez֐~z*l5,|}{`)èkԦȕ8jZu >Z ,c"]K(ܵ~ VbSdy6+ȵ*1jMȵ/<>7H(JpY3I3eJb9ٞهhygyGdy L*[6MV \+FҚutS$ L(%McdEĊ[Lש5kkGR ț/arq1[֫gb0Qg+Pڦ6YȽlS #noY 4AuL(ZN ![j{^ʆ* wy%[E(.t6YU4sξ#1cTXoZ\H#pVsWryc[/Tm76*VO#bvk Uj>C*i/⨁L~8!o;byo)Q=#~Q~'r:,.nCUxfvuP͎163:8K+rS߉6& AW1 [V,:ۜg VՊY\}k [3=YRrq|эy7ԥ{o6wZFTy*)$~G_򤕦V=lUt ڀ*[bE9M'ࡵ6mU7]k{FtEV &᥏{$x=U߉TTd:GϵSi?`5-~~-S׻6~ǘ2! #d#g0gY;~}2yU;3h)-}L4oq*0ӷ o Y3[Ì?$?ی?"3[̏A ؕ¸AdsYD]0ؠi"l=N85#ܫ+F6[&"dl̵4R:`fɦӚZ i!v&'Lo 6C5Ԃ .ͱN@\mWA_r{KJ6"([VNQI~IQ/ᰟ`F'vA+9(KtG\47P\=MUa=>WAr'Bx D 8M{c@qqbǘxq883 WES̀:JU<ݘ A̳GjMV@n5o&(O,wUjp,U9սbVm4tpu`{7"ҞH6_gɕ-SUɋή P!nYJ䧷iy]RI&rk)fOFK9UaAH?qRF蝁UljO uϚ!c01ky~@fKyQgy63_5EQWr\SF#Tvs"l[PQԶ+*::tP C)p7-ܟQ}A:BV2!QynC .zEP\ڑCbvb-1ͼoZ,J7(uF KI@j$5ROÛelUI .K'*ó|64v3ѭTOJd̛Pڈ{l*+(dڝ3knIԈ#QT:Zp[50OeQVSASV5YK&ӚZl|Cώ An0]]]QEs;bZ#]lE6tZ3.ZpXV ZmAN,+, IzlN8u!jΨ lG(2";u>W9}j[ܴ33*h[gjQY=dF$7T4F |= .6:On)j ,͙ʖ:đxNʰ~JNɅ7US$ZI?TLj%tſ'n-0\ʆ9D(aX0χV-Z]Aֵ2bܒ&3kx֎aaRVI͐K:3J9ۣ?'n.bTʞOpcKy $C54j9LRS'3vBF"yϤrt0HlRI6====Hw4S]G]y(zB'.9J$P>n`8my /LYlfSBtSzCȤ|~㶚WC0_QEQkoL>=?r}$)6_d??YB dpsY8'/ /5w7~ʳ>̙KZl%`ޫn0;\~%2D7<\3 UE3*[ʫ%SkjgUFy\Ijj5j陣f˓>*l-ZYCbʱFv7GuM!RY͑ .Tߛ&Akby @iHQgya2LZwo\+Ӟ$~f(kMYI\-6>c'Ӱ1ڜƼapLʺnJZ]7)Cc`QjF4ϵA@=)UQSە7:=m)R|=컊9 .и4 /Epiz)>=nmlH2^Ht՞8z~N83 ΔQE}ly<#,]=_*& U O_igsLR%b3W6ftnҋ ʈmۉ+%TG͈kԜKMv[[1nFAQڃ&cpЍO`em UW5i&;v7ʚLMSJȟclQɧ`N{f9jɍ [0@;kPc$ѶVbc#͈q#v6fF+{aTa:5V-sdw0w_̥4·B #?r9Fudg&=YPZcj?7(; nV nmV6ŽS29.DE.4s*Yfݷ[>Q &F̯t$KuM&.C#4o-͓L]찐h0CV/eue ;O~(䦱ef4&;inK4֚MU,AŦMˋ(*8w*PQv8)4ŕ5Nj dpti &✾FBtFQV2@+Hwbic6-*&ZDUK7r:ۨ'z 1Ёrd~N~8kPRԆ߰dSJw.pp鱜Xl: ,ѭ0Qḑ{A9SS+1ȩ2H0d7$.eQaC=# ʹW9%Ϥ8-;dθ)f5!Rƶ6UI)sdw?-U++­#Qbs;Mw)eqj> *:M,40baOҺ 4lBVX\v*g< ;QdrO߁(~zo,7:ԍT;=eeQᕏ?BEZEi& JAmp JX9܍h Jǹ>e-@jm!օ3t ñJtóS,}?Bwb!=xԕ&LilݙK3P#G,Okcv#δkD9G )A`y}E]~V*,n喿뛍/ᕥLFjd7u:cԤ7#bÝXbg X ֈcyo2һcT<6O׌kċGg1ji6 ogDx@hN}]\_ճI%\9!k;S$,!Qۑiޭ cfCnSގRjxIRHgAU@TcZ8]2o*r[icuQLʑg6S.hNSIB+>L2z &d̞GW%e\iF(:i;K&>>:xMlEhLiکcuPV JG]Q76ZplslXo S \l$k[fv P6Any߉3uΩ=9e1Q߃Ovے~!4p#ګw=^(Cu57̑WzϾ$^봔֗zHݨJ<!'U4A;3?|Wuɏ1u|)` XyKEMŚ*:rW))%mZNhYj&MJҁZíYYbulPխi-΃@Hث++++++++,* n多:v+++++,-$5[]itfm[j]쬬٬쬬k++++fk+++fb[f٬+++++++++++++,* ¬++,+ °+ ¬+++++,*ȵaXVaXVaXVaXUeeeedXmF,TEaXVaXVYYYYYYYYYYYaXVeYYYYYYYaXVeaXUeeeeeeYYYYaVVVVVVXUYYYaVVXUeeeeeYYYaVVVVVXU[ !1AQ"23aq 4BR#Cr0bp$5@ScsP%tDT`dE?$  =e]7G4Ԝъ 冖i κ6((]awS؛tt^qZ+bUPd_b2W,iaJ:qRcSKYְ]ʥQjp]0KkܸΫ]4bvYpP~|]-ܸ*ozzJ%ckF&+/KuS2JgqDlAQ_oy e'8kѼyF҃[luͪl256N-00犫YZZr5iW^5dasm96HbYvt,@!gN^0b\@+\m$e{F }Nj{X/`qnfkSd"\ zw&-(kTQ AlˣVX1ګqk6M۬h3-;n0d=B+ 9{/LuK%Km#6[EVM\W˩X#CuΆ1B[NfߌF:}3W%mEKlQyzSL_,{SAfYq|y&>Mqm> V8hL6m쌜3"(?i`HWkEM$ t\7. +3D0ZY܆4wq\h r6PMe>nw=\ ݊ϧЬcSu-?"ݗv( ":􍋆,|-6vMV&5H rOZزW*nnF_'Wb4 ص}vAnZEGkj-aVnX?LRGu;%p6HuSKhp#,4>zcݕ*Q6M z./ p_ 6jc.Tf63 7;ߔ\%Ć' = D2laeS-KXYx@[ Nv5ms ^bh)VGe֚MI;+ر p{lz惴JĂQ8gf0NbLW [~{U)txTYmPbmspjP]0%xMm5|OWܬ_ 6N ?[ o@ۉIb\-9nج\3/[=v9m~֫]F[H{>}T6>}[DTo8VvրkT\'k~,UL-6%_mv%rXW6嶽JvAh-ttiڪ2߆@U^ih8RU|(֊g HDy?+RUpۑWToq)XPPbBp;XUSHDj~Pܷ٢IPp U. yE Uub.6AS}_Y }c( >.ώc5ޝ,{;o}F⍛Q%9,V`7\ls޽1*;3tQsע;jb/Cw[ޟ SON}kZa-suE\heQ/unW:PٔmwӃ}b{nzoiniHUB}~n,thbz* (Օ4SC]mCjmz1_셯hsN`Ya#Y{ЊʽK"~eXPHtkUkj[' |uѻ[9'4WaeUf}h#1JehATftt{w*Bº|†QajR8*Y.+exlE\m*{v-Ɏ=4ak#m嫶bp>yn(Y_Y+V^=ʴYzlNOjEwbk$fNrX,k$̮eU-_)2nCċ!,!c)Ijf=\`GHm{Ou St٭1=~Ia8b2>#4F'-D7'*#^@C6*,BM6-U|\Wt-TVYwdx>xWGT to[p^S;L#aQJ:E*١}\l)FIZI^;G{C5v{~= %hIi.&D=W%.f981|{GFY#ƚ~3xyF.= (vhw׫\ 7$f;.0bR8SGRK7jSDls>%^@m6e_In)-6[K]rz /8o7&p8 |ۭ7X9r{+S>CkO֯yC?񺪮lC~gTtP7K3.Ŭ{lu( Z6V30#r9ݢP4y.vpXhό4)y3nEbs6u,^u*cU+>MHi1g&MnVhYxA*]mi`q ݡ41#< [(P0cMo;|օاi%jiXnRX_⺥>֜U"c?4Uu*FI#T֋odQSf2L8W"I(WF'ڮ3O;̨i,bEL`;ȩ\yJֳzШTzީI 6O0)tY!w*Xm[teOۣtGl؂:tڀxuMv5pC6v[-ihV 8 8#gmevem}P|wռ[?vy0 -ܰY+s KKDH9Y4Wz-)^ztTiv,q7#>W'@MsM2nCmT >xkGҚGbUv#Ms#8F 8 Z}wzQݔqlMP,qnPL\NE5eWw0eXcm͈Gr S9U1 'tRYU{KzvJ>m?r<%k̊qkPսWN(Y-CqZ\Z3w{K[^s+46\qc+?s_'+F[ӳѱrjȷTBW2g⹦wC{.FxsTᴧ Z{-Nqivq^zc:zUϲC`@ucܫ%dg3 2^[+lzĈǫѠ1IvAF5;dR;q)Eha$NE;.5Ѹ0ڡ垚#8׫#CC>UܐMJ*(nF`uMc*u@mRZ!RÕ6Hqjc{1ܮۣ \Q*Z=_Sٟ&udwjC-&zF r^ZqK (zGJ(=1vetGmN5AڢlQ:UEkZ'FP%Jsd3H5nvCGz'rr fWpڸ25wmJ<#jE2@9𭛍u=™&r!֟1摿wZ l+.;=@+v-Z7zx0ukl0O$[D׷{}*4~:9Kvϙ2dәDEƗlt{  S- /a܍=aM/Q~zι.Ut6֖jմDw1V91 RZGSW6Ί,Vy'"(ѹ`W3艫=4kn+Oxc~D 'qTv}VQ><:< $nlX4/Fy#5r#mnnU+Znwص˩'5q̧ T_Dhⰻ.fObOG+ѥ {6j\:6W6V1/T.ްp>Iiu7>IiPFѳ:(ZN$6n "/\jaMHK]G3\Hel,9Tѝ r X7knUhW`"}U@גjuV^,Џ?+z*֯Ԏ2=cWٱD*5EﳥmEah>ZF>vg0Y;Zozwy#C 9j/q]OzkU\N1qp#t <%-#ЪtnS#wؠmrh:-_PC!Ƌ@>; /Mz|ʟ&sBkdi<z6i)S]yi4pmPd@1 !4u'" 9.?#lH.'#(?d;xE 8k˯W1'2+;mn .J{<j`WlL_( ;:]uΦ#H6| YyuYEc=>`u! u0o(̆ӲH_)I]|Ze&aQtp]g^8CZ(Yϕ౱z#?,,O= '^1yI~2SgG$ Ot ERSK&48hֲodԫ1ڔ==\ow@Rڭm{ hAJlvFgeU2l޸݈{# 6h/VW{Z76QR6!L3rH7*#Jnu˶֫$n#x fȇU^Fo*vq=h7pN_-|_z$|ZOzgW%[p-;TDkrΏ2ˈzT>w)fB'} T_DhۦbV-ĈvN)91Uv,?YbE8$#^_YⲛN".ngq}rq9̏j 0Y]*9zt\ܭZ Mj4}PX4|iތ{@DU}c2H8GUkxldi`l52~HDik! [+ugcP[+KZ6*/mS9y 6>-CވRNޭyǠu}=rq4-tR1vaAs+>S-@źX,uj\iLVX^7SSfkugJHrdmAmzKAdhj#4Kb{9Szv2Fp#qlY䖋CflZwM6zͼF:T~su_ۊwW;~usߺD^+(oEMW]cڹ\Ij~IR\ZwQPip!-{~X8i6a[PvN[”\suc:4@6+T7P/{ds^]c#auzGH`ga8i@-+]RC'?}}M}%hK4Du)wX>SnG$$|)\Cy)쨟YunW]%䛵͍2Q;kK0ttzPWzPUKizPWIjۧmG9_?I+:qS*Z^(QC3$jvWj d!|eY,fFr5j.t*hu~<_rݬƳT-iGڃYil:YS-mKJ,]p/DO=Kܨ3N)d.hE{|='rs&,u$.}ɼqu`azR\wzl)مKk|]eQq\Owְ{oОLôև4x9,;UN^,Sqd=P(47^PPpLxj?vhunwӊZ(.6l5Jm4v sëQU`Ok+HZ{2M?Q u$h=aRz,]UO‰t}zբtoS[$vho+ {8 V()iYxZ!~ Z!Nuڹ&JT<)&&t0 qܟn>"- rUztnGzd_{T jЙgհ6̓6fg&&56kEutÈ>WYl?ļ]XCx9-[?F-5frzB,m7ڭbX[wilս|m:|#ZP)ҽ}c|TYY}풤^btܔf+T,荦 (9T}nϿV}k,r@Cu&Ћ.lAۥQWcFu+ W5¹j?slW$v`ÿܹ5Mmo2ު)۳r 建fa!ǽ_]lx;ܲXu>~kW f[!V@e* kFaT%XFT8G9T:5Mj+Jօ5Bc%5u=. _ZJ{׸:gio(ؙC\;깟ODxR2>+<'+FiӴ~`iYsX Q;QN9DZ EQvX]'1;S^:NoEj8co+u M(HK^u5&n _l|c}у܍xI;| >8_+@hg1v+=KgO+$&Wdd"i*8s04k/dRXlc53;[к7 }h&k){ iֽ3- mrnⁿre^n7aMC#J&Gi p.3) %]S% ΔME)QP& 7;SaV֬6Α!qE\32 `1 t[,|s=upX_ƐwZuZޠM dMԖxPOH>++7~RjPy*fkYRv\?ծ]f`aDUPiu¡dr~xzⰴS"Lw-fKoj,<ŽEqz>f ;GF?U˝^:Q<-i[Mڿ/w Oc. |ue6Է%25VkE ٫3$XYy.%*o%2')]`;=92kmhd65`ㅭc X>:541:FDiν4%vn>`cه!(`e~V^t,xv;?$Ab僻GT 'iX4rGbb4hϢ\W6͵`.]KY"9~+% 7\ryQ\ cZk7l5:S`A^f\Ӎs(?@Z Iya; e.Jz[u*vHVtѳKQkk"ܹڹڹڹڹڹڹڹڹڹڹڹڹڹڽvGjqڽv\YfJM}mv{[ʕ\\\\\\\\^;Wz8^;Uly#@zA 6e|9Ly(@ и99c)sK7QY%eVHn:+jqڽvGojvGj{C~(h{U4^z8^;z8z;{^z3{7^;÷^;z3{÷^z+{XB;=$V[`Y+kuiJV [/EooV U%3ҼU|3S%e^z+{^z+{^z+{^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߋ^ߏ^ߏ^ߏ_^ߏ_^Ϗ_^Ϗ_\ߏ_\Ϗ_\Ϗ_\Ϗ_լZϏ_լZϏ_լ_լ_լ_լ_լ_լ_Ռ_Ռ_Ռ_Ռ_Ռ_Ռ_Ռ_Ռ_Ռ_Ռ_Ռ_lW[>VϬU?_lT>J֔_Nk\.oS޹ޫwc=\*\\\zyaԃ T˛\z7޹w>\_]R%(oWr`Ջiђdz!wg5AT]Rwrysܮ=*r לU!P]L+Z#򿺹w.ss˜\r;/r/r/r/r9ܹ}˜\r;9ܹw.ss˜\r;9ܹw._r._r._r._r._r._r._r._r._r.sr9ܹw.ss˜\r;9ܹw.ss˜\r;9ܹw.ss˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˕ܹ]˕ܹ]˕ܹ]˕ܹ]˕ܹ]˕ܹ]˕ܹ]˕ܹ]˕ܹ]˕ܹKK=.OzOzOzqzb6sݻTMZ*֊!M;9.V*``4iF̕N(<*snXaQT*anQGލv+*dJ\捋 :ɼUJN;^_/n]blGM,k#Rkn]z4cp4\Q@@=:6 d|}Eq+*&Hh\001atՏag*mWW8{QkSZ64gW#qL+؊nUحC}ۭo\WsZs r sV;kOgb“yy6vʞw2׺xeja/vrR/lªxӆ*rۣwBgRU[N?Ab_&㫧¬ 7 ?x^]yk7 n)m?7CUK\oVsKI_/#Nٮ?&:qiMnԓU-iͬ: ;dERKnQMfY#`~ lb6;gRChͽNp6;4 xi\1KzpM}cڢ6?$W]m{hG=˅G ߱~M[ (~sS8V,V-\GkӈY>_( %OmT<)(t8tp &e` CgdW,9q$*5K.+ VK )8O \lhiL~KQc# 5ΊTG`S7d*8iBtu]`SԷ,ڶX{8V~i٣L#EtqqFdAy٢1O=+ B:U 4!R5wv;ƨ[8sGد/ 䟵k$v:(3N5U~g;PT}k}B8c֙I+F&x>xmVND n ګ䟾|]i!·WI>۷?Y85<&ydlkE?`ۼxo3YU^h!  0 i>eN rPhFⓑEoTw'HXh{n!18qWf7Yd+#=L6x'k/і*+1-X4 8aS&%R[3ҕ0UwCX,X8QQU*7*;o$ggn)Ӕ*4t.4tUWfMʨ+b@\-n]%t^o(c֚dP Dj%c9mrMpS~&WbeZ}d֟V>(U~'lW1j+i<խrsMA?e;4F::U؀"}a3-)DI|ij˒~ત Mt#;6q\KVD/Ey$Y^v4dd\\i؁"9OZJ1Nf9v F{%9if`O+& 5\H\њܿctޙ٧[^ .iWbWGGFjeY,ÇBZ#l_bۧ>])0_!J&2 Fj hˋZ@:mP- 7dy}:K;&.W"ے4{m>j΢FOꊦE',f4O6ϊCx9m/4'1뛁1BhK:6-+ǕvG ~Eө9ëC_ጂ1o7rci .6n~k,a!@zSn 4nߦ JhugB j4d;Ai:L6yI( %pm&G nWXG깥jkxA5hd䍃juٻ5ѱW~N͜4V#z$ip !kJ{ U%ש~YhJUTrq {hۑU5:nV Y60٭JF͵ak&3;Tns6rվhgCuWF?y.F9tR76P=&7PmPpbz[xs@x BeSx7CGS\᯴lԫ4z=OZ: b&mgyswF9yb-w(d]6ԏ\zt^_} g$qw =ktڝSV 'ѵ:݄WFimEETdwq6DSCߟhEWтsp#.wRGj@z>ɭ ֩Xxf'*&xG+wx]>xG+wx]>xG+wx]>xG+wh]>xF+ѣwh]>xF+ѣwh]>xF+cwX].xE+bwX_.E+bX_.H_K"/DĽ.H_K"/DĽ.H_K"/DĽ/Ľ/Ľ/Ľ/Ľ/Ľ/Ľ/ĽĽĽĽ Ľ Ľ Ľ Ľ Ľ Ľ Ľ Ľ Ľ Ľ ĽO^'/AIJK֗h+k-iabZ*i,[frvUh#&ʓᡏ8MqwC\k4?kT9T٢0e;z+9b*i{Yp zTwF;Ka#x)V؞0-h~oqK|xTS%'So:YXY!g?*j ^_<"[]cqo@omJX&Z ؑ$=z $yŠjSL1Xvt܄ˌWD1 ++ i͕ͦe ƕ.ްp>P # ***֗Sp䶊oԻf<㩅S,6Zщ*1HXe&_;\_G:sbz,#zoO9 DƋfZm?˾Tyι zoUű3ېF9߃{C*13O+8~6wPzT.V]T+66NMiŽI_,;'F7vv{l W foNc rɶk+}ˬk2+9NvcE[(ߡ{oҋX=\x kNƷ:M_7kݗjFyG3wWjyn.[\V"&WwRg!kEiۗܽ>^O9!M5c4S@NM= 4 F)<4^pmPd@1 !4u'.8{ أk~E} d?d5E.D Q^cNdvwHmn .v^20 `TY ]vu'UAM3 o@%gSr.(r}ǧ̽>BahQ d5%v[k EvwQXdOާkE@uY#+J]ܽ #@M>{dsny75Ty.1dÛM<$QOh1CK8?~ޥv9ڌ\x/2k{t 14]ղ]ax]*㐵_ʌY ڸܪtq]ӱ|NJ[NF*3i{9 c2?ݒY#{Mz>$*EֲdX~M(͉P32`yu޶8Yѧx~+u0*wikrH8GUkxldi`l=2~rf\,Q|Qվ2Vv= 9.yZ(T{j!siqjy苵$-߽PGK|z Wܳ߹''B}!IQGFJ8Pe2D o]LTqmWVi{YL_*1WU -`ڬ_Y$xA`EY ^j. ыsɗ΂ȐxԘ3FZ0Ntr!7!M8V,k@8 bbMEͽa^m.5o; W]+\ge]m;i&#p_%qwEv&Khtzx]yرiye&Wvrao(!rP{`tޖF?XYֻ%je sɦx PyJ0( :V!8^rB"h^a#~Wю[K%#XOVI /d(<`}nYXҝ=S u% s:>`YѴ5շFh{VsŽl9d{WZ- ɰkk}ig66lRIί|n*1]?IYGr*njIR\ZwQPip!-{~X8i6a[PvOha+9bfhrjC>d`l!B>uzk-kDY .eN-V9{ʥ\$6XIW`a czN3J-{ 6 bV1P&fէ49 cgo-~a,]qCDLkP =>xuj* SPMe}_OfVj0.č" V^ŋYXQ7ΌtoVG:Y]Ml٣pPXT\Ukځ96t ?7V()iYxZ!~ Z!Nuڹ&JT<)&&t0 qܟn>"- rUztnGzd_{T jЙgհ6̓6fg&/ɭM+Gbutjͮdca% j/W>ax=_ B f]z~o> P4(UuhCw<۬n]-Qw Z6 ޾7~ Yjq_'Xg"ےktF}g߫k>iM_#$>%&_;\G:}.{ cuSqNtGbf?s|+\_ W%z5d;|rxMʕ?{ #Ed=Cܮ{MY?Alq[AX=:YeHVlоڃ'7O$Ќcѣ;@@z`w0$4nX+Z) H֦4b4+9}i( W^@_sWޥxbg pby>1mJCʧGdiNuMec+EG8-YEavN >~ihF^d^wZuZޠM dMԖ3xPOH>+fPvbT#vwkUAP䮲V=-k*W*KgʳR,kb&ݯ|5qx]?Mh*h,wmX⸷1Zŷ(;'rur[tyke䶛=n~}kLGpme˫-Y\o )YfͰٙr:A3$hYI/o(?YJOyƩC\~I6Ճ8gd-k F *Y:tӼ#q/\}_֩lwЦHfMZE[Lm82RzGNCLzqq]m6 $tC^>UY,dv+EgUY joj[Y~fY`8cSUIgLɟY3 &|dπ,>gLɟY3 &|dπ,>gLɟVLgՅ>,adϫ &|dπ,>,adϫ &|dπ,mY3ɟVLadϫ &}XY3ڲgյdϫjɟVՓ>&}[VLmY3ھo<7~  \hɟVՓ>&}[VLmY3ڲgյdϫjɟVՔVՔVՔVՔVՔVՓ>&}[W[|[|[|Q[|[|Q[|[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|Q[|QS|QS|QS|QS|QS|S|QS|QS|QS|QS|QS|QS|QS|Q}S|}S|}K|}K|}K|Q}K|}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}K<}C<}C<}C<}C<}C<}C<C<C<C<C<C<C<C<C<C<C<C<C<C<C<C<D~ _3/xg^??/|~ _3/xg^??+p߳ĝgѢ1ZWzܰ#bW!ʥsn[kԪoDƂ7GF.ͪ/=lQ4 U曈[Dln20D8=)< y4lրV8?'l 8֮Zx T[h ֞kgm }ڻmP Y]J;m s,|W+ ߯xJY < vGB]iT?ǭ/dTaԛ ɸ`Z]h5bhM7hkGapQp~SɘGlV\7{Yc׷+ذvWxHssM^BU~'vv!cGoܷԸUp\E6SjP :oFX|$#eaeAx²Y66,>k/~BYWdB̻z xnVmr* yMUU>kd׎#ܺ7~r4bd? rjіܰMf3@ h"0Ь®nPhPWOP7,3TWZT'C.b*iSܰAnLqX憁v7^+Ii ޳њ^s̒X];P/N^z>*8OV~EaWzv ջrx4aޛ&ӣڰ]ئ $.[k֣֮aXmHY墽:\F;5$~T d=9*@{/SwFG;}]uw- N,! Ê3YR$H&@>kب!vpE@ {W檼^=+ /y l\C;Y՗zJK>GL0qci4EyoC'>Kc=l9w; e+N|^]b<`j[_dL{^m& W~+D%+ѡ`417|{GFY#ƚ~3xyF.= (vE߹VvOoT q84u*&/&oM:YY)hk^iԨT٢3E.w\6QiYlM9w=5h֪_Υے!MhyN-hTt,IOƁkwycy/=exKV up޿N^}rACFS^#v;8p/Uk;ۺd;FsEJ6h]DJ#+h廒;hrZф叿8-L;.Fi]I0mUrNhpK/YuPG)$5^޷fEBu PeV]3̑|}QZ\K]yQӌa䍀BV+.l 9Dߓ\P8lǾ6Gs+olf> ke>\7f{nYVYvH[$(pm9# ds]Q҆5Z5Jպ6So8#k V8+ԣ=`(b4WD̲ݟqWɘk7EKX]J=klǍj83*oGB{:3sE?Grvk܍)H07zvf3GɎ`xl`$5=~lg}6}盋+kt208Fv^Y V< n{ e0!ӡw%ܧ'vf#sqĕwԜ&Z&Մ_S3 c-\\6 d~CU5y[Vյ5P6<Y&d6ҿ b=R̸ۧ}UGWYq^@ktq8暌Ssncwj6e!GGD7c8b`surۭ_95tme\y? +/b #{{.sNm4W`1}4YfدzԌlOQ* yH*>ھJ&Wq{v+t# T|g \{ڮ7 (2/ͧ"92DE32]n(mkTzczSyCNh]׻58Q߿޷ m |41'dd8n boiW/+AiW*1Zvy.tH~?yYu&#JUF8Qhďr~ 8qEP+)Ox\,iWKq ]Y dO~9nI$.(Fwdq;_ym̓0#kqIe|X9k>zBhmiZmy[(ﻯ;1'5)ƽj)Cds=#}H`ArhT,`lW;dfۨb%s(XGS}3 {4Ʋq;=o񫇝um])58PsMAF+DָF<}$2FR(/6 &khDy4_ 8AzSzZ,!vu=2fɅd蓫n% 1MϹ>g:W:>mWGlOſr)puR"nuVhn`zq.M-@?&;tP:5y/ ^I߱|Lza?b~ߓZy9i4qޅ4mN =0+Y''`>gl7TCK4ZH [٨=e7C1K$)cpkil"6tkc<#f6;2O|} c9OvƴwW7xƃJ2Жv+7nbd5͎w+nE6~4+VjdTSkGE(FYfkE#6 r.MxiKnRɎRV̻92aV#}n&cxswV?b4aq+u"1ScR5٤Ya[\e61ۊ韵Gguqfj(sr#7Wm#Ҭ g\tUعD_dizUq'h'WDAsة?++ia y ߉yB9!_rYѹbsqE S{nX8=Lg'7jNA_ձwr#,bkbHB_cX6JD`F cc+,m-ZEx4k]vE1ǗZ],ӋXV}6?+ 峥H͎$(D6VdHf?=J^!H6h$~W9Dv+C#Iuߥyqqܬ18;ZӢy~!Equ68*ͮ)U4 ִ#4I. ҇JeSÚn~kr_kPVc8RS%z*(jj/+쮔akn]a ֭Tz$'z1Fx޼JA~ =3l5z韵U-;FToZ}E5uq0;梳蚾Ě$+FI‹v;T)Z.XYz/r./bb-p{~?7%] FyLvIlvqyk13ڳϲW(!WDY}ZNPjLfYd ,Ar:1r4^8QLތrw AiȍQ1 E4MSt6aՅE{:Sm2FƝ[=hR :LjAA_:'>gCG<ٯBύBi|{ kCpERXĎ{Í^H;WZfUi¦kwN(𛻯Hy'q^:Y{_mfCr|[y#/48Z)b_MXP-JQqZ&qFɊ֥R@hPeexp PNmfܸd$.;RϏ|UKۛyZW<M}\W1/P_{h׀kmMU2ϡ4áLlZI!ǜՒ/ ~p3 w5c'9 }*KFd5uُi-u4"Σ^^e_o v*\*`SgrB X T$Z,MHg1L\'8_ dsv Q$ǩYx{ Lv'㸹pUN4n?8 -+ƿR'K偮pL6Zp`4k*`fp6mD5ǺaSV+_&mpX4NRZeΕ=8&nDֵqrn[Q_<-;Zχ%['p\'bBSb7tXN-õpև;%l(j`=2Rf4\Ţ R݆Ik/=e'"lC;֐Gf  7ok%\#m}QZ~nm#Ϯj_>Ii@[im S@~qೃ6_W;m#MC@]E~1Cf kKH<^:/'?Sr+wZ,9L1ޭV =Z5W[VFh/'k-}n\%eXf򖺡.l\ڦKcbY$wN2)kỽYͽlBmxb6 u>rpq嵏V'2(L/XQ࠵~n%g }]{~Mp?eʚBn lV%}SJY& 6 08RVnec6~ fJlv~Ũ(ylo~737:@:*bT\41I#.8T,JC[~sdj_A7z&h}en]%t^o(c֚dP Dj%c9mrMpS~f)T?ڝ# ~_@ SҾ0cLJ^ey@PzPG8Bݫ"؉D7P^'9JzخZ/cbUX'{KD!tGx^OM雨muAA &Uv.]JFXrFPU׏PXq}vϡb)Z"o'h_)V^:B 1lUE}]xm ^D)ے76F] ;ӣ?6ppUX蒋:5=2W߉:.yk(hyMU,JGx%KmHM%8?wWVvY[;rh[JmOI#hT5 ZY*23r{ hč GI'f΍-̭nYZ Iͤ+' \ x.vܱOʻzܪ F񥚬[S2XvxOBGخ= jd[>`=hYmPW3#1Qr!#;K#vۆJIei+$YFKEY{]$`ɡbnklpt77ܚrWNPyce 5,r|tI-y,e;ikb'3ʢc\^31h; 񍰋KEgӼK̵ޚ85Y/h8mWZ7sor#=UWfԞV,xk.,mHdgoIG'2qO{~Qݢz`nGP ]5d2wAMxFчWv(jjsT(nR^7X%ɡb|e֨6dtMi^+R*WpGd1z6SVK*T9}uaԸ/5jZsSHC|Sԉ|~`Y ׺T)_@1++ʴ񤉿hEm.s?wߣCۉ\JqyD;h0عn\v[W-ݫwj廵6%4Gh&7m)"NtYaUv+%@ҹn\v%h|/B2fg%>):R <w3t3YngſAOv9n;T^5|WZeNX}øtio5t:xq6F -;V:X"tsn9Szd:55'g۵cY.˭5rˀ5&D'ݛ_o(lʓ N;rع\v.C6wbعv.m݋wb;sn\ۻ6ͻsn\ۻ6ͻsn\ۻ6ͻ-jr\7v.m݋wbعv.m݋wbعv.m݋wbعv.=o15U,wA%o6ͻsn\6so\ܟ )>O\ۻ6ͻsn\ۻ6ͻsn\6ͻsn\ۻ6Ϳso\ۻ6ͻsn\ۻ6Ϳsn\ۻ6ͿR[so\6ͿsoDz!Ct!{4+bع.m m m m ع+bi i i i i i i i i i i i i i i i i i _e _i _e _e dW2s/W3'¹>fOs2|%s2|+++++\̟ \̟ \̟ dJdJdJdJdJdJdJdJdJ΍nLKTc$  =e]7G4Ԝъ 冖i κ6((]awS؛tt^qZ+bUPd_b2W,iaJ:qRcSKYְ]ʥQjp]0KkܸΫ]4bvYpP~|]-ܸ*ozzJ%oF::4o*XqnP&F F97j FuVN݁*U.Ў,_r3(FS!h &Aj=03L^GN ky 71β-Pϖrn`o}I!h었v+0nXN@$1r:=[m D袷I`Ee"Obd]57,7Ttc:elp Y{o[e][=nSCnkkegиBml&d㍞Ojn)׏`4TڊV~~nuk2;o\9TVl_d*4U8ǀWXB=hupU?/[&waZ7o\K84.{-(,PXl2Hڋh١лUBYhF-$rGGNzQӆJ$8aU~ٛ:z+e[d$&7Xlo<KoM;s`5Oe)'4+8R4ϊ&T\.Mqtm*y,'o/7 >mS_̆o%)a1h.mz>oO3\ v\)?[uMF`*6.M+$& <f2yUBjrڢg~Sa[fCaPqUc`3 Wv?l܋}Tioެ3u 2{M13=KmR_mu?5j;c[Wot4[IQEœ!,x`hpۿ9imF_#^p]z,HMiV$dSR^F-lr?|4{lyph3V]?GyGrɢ8K[m(p\V.p &j~9Wifso&-@h!hxZQ%/ g{&N;{e~VofmPpo -G R{^Z8D]4׮'TlV+-8R“|Pݤ~\ֶ,J;:B5ةME6-_b+ݢx&x +vVCp{L#\(>ή-W CkGVik=˃zebz+dkKqpզ+9Y_*O`_bKIi0hK9S3xf..zd.u\Jkq~[\lmqeK,?&,a:8׎Zd v_lƿ}h]jFtZ)ڿ');SEX [g!SFV+.ٍ=ZOrح9 KMÍdPݦu{EZw%}?P(bpk6F_f>_gYmcy5T@6ڼW}X?CȢd?"1ާeƷWmH?W9ku~OJ=9E56MVKioG&~@5zmPр ׎?aefn޻0Ht3ViUe֋Hў5ެlK173fչ;?`J9TmmzW胘[&ٵTepegeՖ7 ,9VNFc3Z~.4Δ EuZ?XM\l1U$S7U$sG?3V:?(5)7Wq [Wo1mxmm֋eRL|\(_i:'v~*NZp2[GVCU'reיdth?WIIS*)X5p)8'ʟl,\9n3k#LY$ۊ~c;y4s]. ,?ikmBed{bV%YBߛ :kĕVoC/w?ˠU8oEUnܫE( .wɴ4Wv) *aXmHY墽*"\_!PܱdjPg59BýlV3t⅟"6I^W{I{GYXJω`k#D}H"L@bq!›ӡR\y,\{Whk}QW'U[9fϱPbu:X6Paӽ4EyoC'>Kc=l9w; e+N|^]b<`j[_dLpyp\2Sʋ] 85*환{n_)+زXBUXzhiܯen*7/cǽ4z4e?j,i.17Da  4`u,^q]hB6W]} -o]X?(Zლsi]"/7zmK6SծzRRfi޵sFag4l`YZ:nHx6M8RXmвNg'NƳn:knBFjG;jj Y+ #(kd4mzb܎whTK(MḄ -Fj*6z ׷|X8o'B,BoMpEBÕ-tB0mUrNhpdDϐ <ױ )ͧqYz|]rp] peuz S,dJ6h#$År+,t7[oV7dm+Ѕ,hx٣ kk&23):x6yLTQ:Xڏipŕ.{חY]iZY~{^kG)ܽؕPPÔM6*Enr$f[t;gu+RiW;`YۓZ!nm؀rn6) {Ϊڮv649|}\,j њHg$|RzJhb͔ 4~:h `mm0bZXYb`7B"v9 $FkPd& hyeKLQ؛%۠9=͓74!Kݪ)c~9' ̑̓{JoB(6h=)u-pLNZEs~`N@mM~}D O&D J^EABꜮ dPlnfό,>6}cVlƬYg5fϬj͟Xl͟Y 6|afό, h$r[*-gPӦYfB2V!x0+iέU5?yLTPq4ACar>0wZypڪ BtԮ$R7Fм쑴X!08ײu%>0gl͟Y 6|afό,>0gl͟Y 6|afό,fϬ 6}`Yg>6}cVlƬYg5fϬj͟XXXXXXXXXXXXXXXXXXZZZZZZZZZZZZZZZZZZ_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_^_^_^_^_^_^?^?^?^?^?^?^?^?^?^?^?^?^?^? _31WGgb??|~+#_31WGgb??|~(u1%kqdG cǫH2mWz>Z縚ZN})T׎K+[0+;c a RtB::iɉX,Fa5\(Cajul lNoݡyl:)Ն{Nw5>NUf2f#OA# 8-= *E2ֻ;'5(Ir9 gD>yc|YZGhz|iU-hb V(2_syָQ/Qq4eZcɟ_3e'6 {y2}=NcM4?lhLKZ~~YD}(af?=[{ztN5k- 1_%`TbN }[7N5Z қ146h%)dl6֢ oa<n]CpO#\c~:}Qҟ'jpV_{i\& Άg omQqf6i^u3k)+}qq4QqK[(tʌA4ָ,xڋӍ2t)Ƀ79*_{Y}؄)<'Pd{BMY\Fmo3}+\Eҁ+/ʼVuTZ*)h$[d{WLaZv hbcGcW,)dV\3 r ̠+[k:MƵo* \(G糒!l8҆q@2JܚCx^ۿ~0Y7!6 1AsCMesԡo%ݦ9MRUznA4lL99d)2Ki=yLAp\Wk~N%:vʽp/;iSkG6`n!NRhmISp1 ?;F+ˍuts3#=q?j8GH O0,ȿ6\lpP68w裂1Q\KrIQpŅ{ ߇ZѦV({xUm>C[*~4ƍgD6jYSmr+׎+\pߑ_(]`cZsdclhԨw(ZME=A~Rhjuz=\sNyk %+? Hi`t;p{ gy?٤KԻN(.`ALh[2k"g?S>0oMyPPHPbk6&SF|guZPc=z⊬V$0I-÷O1Ҫ;K$&䝛Fiʫ>6R]mVr#rd>kǸ,$-JlRjatzSS^@_:3SB 2@ێK4<_io tOkv'Qf$}Mk4Ī]x4eW ¯8:$V ^%leJO8)Ο#\H6i4yk*41>Wie4x5<.7iG#K\* yvq+sz$?M%cKr<'KMҵV{ t MH w99sMn=$GMhⲯ`]9)b7ZX,CoW m2DRҪx;Ϛ涀2>6೾ÓUk_i|L3}*s wr5T{c+O܋c=@dvYg{+ @QYdW\c|\B1Jwzd80iS5Տ w~бNZe{G~ o3\Fҿ9;4pg 23n%-:~ .ޱ7VL\CEQqSGT vaMk_݂-Qo{i$n n9J0n~('%wbWƤkTv7-RʮfxOt#bCs;Uc՟G~YǶ`NZjцfNNf\-Tj#t~/duP69>3u88(|y_g4MhQwm,i^8[fHic@Vl37evenݗMGE?T8j#E`6QA+Y&2}^m$"bE[f\Ca~e{0_q<\:t[_Thsֻn=$ERۭA},V oWC{KPcz1}o2~5W 6\QC.,nwP_(s{=dvӕ F2ChB2M_z)5bނ%qEGM{dUYg/wW%;XnzL1,=#zyWr=#]\ D~U9&#ʌpLO5o'} }f+GGHX׎]L8,/.<a0ưmwHZ;'}XwoHҠ?r2k}jy踶uWa_桃j󳊀sߪ60fc`6 Olbk+g¡ ~3M8%7G^םu,|$o!^ǟFxn(a=t.]h}.خF8od1E2+]K-ߗXY:*2QW:qY#Ky]jq\)PpbjeTe= aDp@ʆzU9iSƐY97|ٿ?hVߧ&hWE6@I7}EuU^m#"ˊ ]P[dJ"  [-p]>EI;ۿ>NTɣbn(5"\v =z78])sI%sErjd|D0Vi6fGGT,:.9`WiծAh8G[cynDݓVa⊂AEε%Id=C' Kmo T.{Ôa V~ (3u Ҹ_Ӈ~7dD`#=9|g{H+^O걥DY}ZmNc޸&j+%CpZԷtܲX+RS]ޤh0ׄ|Ud= qm.\<״(VwmuJ$a08:Y-}>@ = GԿ#r1vr CZ09;Sm_UC}lKvp{{)mx+;l3p|Ԛ+'Fjg AyG!E1ʨzp jѲ006B.pki^  @/Ta;̴pu -B I~jX=_uhꌎikMTx"cfG>Y ^S94{ Bm8swt*=T -\H4;S,Fh~Gb 6x&l7qK,lt^@g~Jf/}uf+'-;槸)ڸ~P"I{z|ƦȍRId\MkMIB?Y紺MM+km2kd*[*}<I~q]e;X4xrV&z(Rӣ5o)~e̓8*]V\b\4d`%_, ħKvN vVMևn&U4u\~[׃( M3@(#)RUk-vS3r.3vAjVLmZd8}2YN0I nTiYuauȽyu.b)g( wdc_"'YgH~P~uQz?#t7F– nZҟWÒ]r̨E9ꈬ3 $ol.5 0I4;Asᕓ2xuBmrlx&_&2=SM# <}׸)-2qo`!Ebg544е0\4[>L]i8Lf ?K~-cټBDC]tդc9CFcGj# Xx7%V^t,xv$q%n&5މyk5,%=F0-"o$v&EFr+o0rGb ya-u\qR1P,";?A C~5+V.?ww]|6:γs\;:γs\;:γs\;:γs\;:γsQ|oZm,w.u gYw.u gw.y g1p:)m!|>ϳs\;<3s\;<3s\;<3sF'ZH5mk`w YQZƆs\;>ϳsϳsϳ=a\?{s> zLc1T2IoknV(_'.8=nJ gx.}{s gx.}{s ?x.~?\v;s;sww">x/H #w{zD}^c 5."Ц5]M^cG;zD}^caְˎ k+S/ίG;zD}^cG;JBS`{o=+#w">x/H cw&>x/I cw&>x/I Ңw*.x/J Ңw*.x/J Ңw*./J Ң../J½.. +K½.. +K½.. +K½2. Ȼ+"_+"L½2. ~ ~ ~ Ⱦ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~?zt? ½:O^'NWC+ӡd2 ~?zt? ½:O^'OW+d2 ~?z|? ½>O^'OW+d|2 >?z| ªmlV6,#t]氎~ya.E=޹{1޹\oH0Jos}As\ȯaJuheށ*YioBw,V X,JwMBqYz3To@U!Ww+9ܱ}=B yQn µ?+\r;9ܹw.sr/r/r/r/s˗ܹw.ss˜\r;9ܹw.ss˜\r._r._r._r._r._r._r._r._r._r;/s˜\r;9ܹw.ss˜\r;9ܹw.ss˜\r;9ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ}˗ܹ]˕ܹ]˕ܹ]˕ܹ]˕ܹ]˕ܹ]˕ܹ]˕ܹ]˕ܹ]˕ܹ]˔KђOzOz^k`*Sn=ۼ0@MuبyMh{#Sb A/٦N4lT8 ̢0!VJRF/%ybprJhز|5T$hUZU,x݂Ʋ9`nu*8ֻנF7E jtѣjƱ:F#F*ǧEUHG61ZU|? /K摁 >xV'AX,sTULFGQ:H09GX ?Bsѵ{zWpcdZ$M(ٍH _ތF+P ؜2 @XګJɛcuG##6X,MN J@5X#ҵ}㦇M4Rb]{\/1AۚyOA9,8ps[%8qٱxtsNm֓me6>.}Qx>kl"gBlKje+֝V;m7ׂY43GOe\'œ_m i޸NG]ն;,~jNz'7{Tze>6VJg}zM Bx+Ļ!hķ(aZ-R[y݉ڬv,:S]k15=Tҳ쳽At4A=iX_4~慱>K'=ÍUJ>FElk7t!wxBuAHȮ=-f8CYQlZAŊֻ۬( +I!g3ƨ`Hhsk,[jY͆3©ZTE<7\rQzepg X~g;*#f¢ >fz~ qs;ְE5`CFx{Q5~cKl&xs)BB#n6V+j֋)l)L RhsG+kafS)BLqp[,:Q~NXYediKk&}߱1>k4=KJmVGJ}u5\r颜4k6Orm?D/ɛC}Ǟʫa4ܸ+ك:1?ʲ_m:ޭ@G_Cl5%p+1 OKz-Hܷo. k=i?lѾiv9+'yE198t)&W}˅6/eS\%im\޷K60mUm4FX[4Jo$  i笎akqR`[{W(U+B*w8mA΁-;t&}5+^Q\nŻpmi鱯*\+o'ooEfp!rx~P pGB-DyuWhRY8Aڛ-9^SݿN<D x-Gke#eyO) g}!h`sX(F9mQa|8hZsW,SXζ`Z2sbC-%b\bZ ~kHDSt~mh.Q"or?g, }p6ZWC_(ua=rPJǪ6¶s~ im%hv> tL on; rMbx%.n޵'6fX2IّsVo?`SԷ,ڶX{8V~i٣L#EtqqFdAy٢1O=+ 'eiZ&^27q?>+nB2W4½! !ߙ\*}.ۃA^B#g ͉ÔPaqwhG 1w%~<z+} k5➕B8#lLѠq8[.B,H^Z٠x`fꉽCSZ_xM5ތZ^s-%bGdm]-k#Umqע?6F*^|] ~yiP:Sgǂ0]>37n[UWq>0mfx@' @šOCܫTxQn[*ëI:*t^@B4L~o'6(c} XUcdMA {eJieEEV NyɯlT3 !5|4uGb ,'<(*jre@UJ ꊎ巿Fott  cMUٱSr \gese8ˤ z\̊vHTdY,tG ͮ] *}>`VUU@ڮ٢=B7VTW@zjլ-=W+7e5;4F::U؀"}a3-)DҔ%ǭz?"KxNqb+wjȅ"zhq{T!QQƯF=pҝ;!mOOB3ٜxC)|^4 DW_:n7Q`84vPT1]AM6]z*\agڅ,K$xz:!]sUvHjY3S6i]Q]ypQء{$xCF`/uMإ>]6u߂4pA4=w[].>0'xw F*6~hPm gFŷxntoxZu'08uhs݀  i kA!/lUЈ63++WYIصqX d)US˥t+S `E`SqcVk#l֨MbpKi΀ծG03NAVm:YGq f%o[3=՞V 2Yu׵XxAZ׷kc+vpOv v-;U'1Wl2?j 6yӛ2 Y@YwvjkwJ$J|_|bh FM'spW-kreG~;f86\y1֬nh߼F_fml*_:hv+Ax!vD8qզ++T/qzWޥujcwm.9>B(\AQQ;ܬ t0D{H@n@֭7' $¯ӳtqv}Cޣ`S=9xtkv8{!KtH6SMXA_jotD4QB*7K#&@M* F*8~q(6)ٍk*xerqɍ܌ōܮHc78Vw 1U;Cfk=iQiӴ!v8Dp 5PE1A⢘?YF}՚ ">nVpi^ҸF:Uõ_.ֆ] e19PUY,EfRWyFBGеQ G֪J+0Qf ت3N֟jOBOrEd}=x6[eHbxUh`آ[(r|1 \K1~ (UGU~1GDYxUizF 9L;[߰Q=\ lc! 5S>V f.߽N9|PΈMWy̬0/~O&̕Mw7ctZ_6^czawP\̟ \ğ^/WKWm4Հu.lhc(^4qp*NFK_$~1D 'Q~xsY7E\? OC΋.pQ&M8cqN"y-&3/Pe, \dѢ Uͅߟb8Ky깕B mYY%YLB$e8*՚!zK"5dCZ@wdYNw?<6.+yF=l :N+qTwc7*[,\XG>lМXPzh0Zpͨ)=]@/ }<\o OPAm6h/UcO06'f|Sʇ  n4^jHzlT3[&3IИ)INbcIsj"\ -h~c=5WAq.?#6(ߢlH.'#('k(]hAƣ^]z9YkpVt04P}kPW;b`@ewYԞUcu6 @~JΧ(˯+qG[yFH&d6BJK)0 *U lO !c*M7+E9!.EFݺf/%i\ϟHmWdsWb*_4^c\RB5 ^+)m"M̗ Vy!o/Q'9KC(v՜¡}WEI<Э0N.܈ӬC7j/:yVǰ`c_Vcq 8lU$4^ڧ{Hs\Al|Zz"I;KzT,Cзw_HRQHyHV̬tLobˮ%թq1Y`{gJ<f56p5gzHrdmA%-\#i;F/ ;~v@ڬ ͜=`nhZAos@H:+ʡ*3u. V܅B_z,":e7m6*]q_nLWi$pѐ_Ղ,9; vԢg9{.&$kX@qX/OY|d>$] 68̢6FQnޤYdFL~;W%]e?*R#ڋgVZۧekO1+<- Ʃ70VF-lO9Fj:Q07zզ! W4'U#Eh-%A 1`+zSǪp`=܎WaWg^2 $dFVA] ߣU[ϻWd1L(CGy%ټ1vg֝{Sl͞/soΕ$]W++(oEM>,V WN*Ս.d0Eo7R?k{N~*F-+h lW+znp|AzNuiɦoP }hDbC;О,-^gʒfK%#u)Lr#qV%6_z4GE OeD߻rFծ(+z~4hh<#hcΏ“J=s}c25ⷩC4q:]J~P_ݱj&_(罸/,Y޼UA-pCY#rM&ǷURT| ec|d ޞzPfS\S5Ŕuj>sDZsXλr.p xJs]zx XW$,}Rѹ7.(zz7ycӈpW섺_bydrStppxW°{5uzڭPA׏ba:m:eQ~uثV **p`BD%Wb >SvofէLP q#h̷r`/`rNGә@hhP<:__&ԌE+ `X bF{+gbY(F?[CzZ>z΂kd-b`:_ )eR>GH[H; }Q]rdC’h r`ZH}j. g}gNqtZx>\oޝQޯF;W#U1~H(њ;K:Z}7Zbq'WL0V*L6^.6%|o/iF )xge/Xje;4~n>Ѕ[ cBhޫꌆ:ѢKLf٨C7:_%qbflȄz۴p;ժ4po_N#eF٩MX8hɬNf [Z7=cE{8Bi¼HLq68g(vZcuSS WO%^½.|+W3¹ ԃ'kfi7*S pwBVn7qyPw 3q؎1=_!8SN@j]pM.o0T۹2uj7 W, dkF KkSurz1]qu X/R+fPvbT#vw+UAPhZʕʳRYrTK+ZØjMo 8M\qMo?~ 5Tƈ{ܖՀXy.+zC.1\䝡^"G.+CGgY-Mi_p;>ux&pN2eveՖ >U[}͝#aI-trIL,m+>CD\ ceh$G-1mѪ}ev-rsZj[l Kd{psG%1A8iv\MJz:j`W#M7n{LWj?Y_'利򛰪kcO^+2Km*!1AQa q0@P?!q ^6-"/MS~INoV|k%7T}c^Z\YI+={Ja\\ʠLU CmQ,#Oa]fd1tlwɱoJfL}&NްޱCvyNz, qNrJ`QsDջjW)ss)g+`4} HIJR ^aow"v9`:m^zLwV 25Ni~bAUp5+6Խ;VAk˗.\ZWGG cr.^\\.\ ьcugWF1у~fX ^Y06YCf (C]ECzwL#j+r T-CC1|ѻZ -kxci]Gr)x 86q c-ưnܖx>ec q׼`y֯HJ|0BWIru؏折,|v3 5ۖǪٖW>:y0SF`R-Yd j] QԺ\w0GDHߌ67uqP"6ZT{7A,_ߌޗ2tX~%/Uir,/EcUr/уLGF2]8o~ <xM vlMef8#˲@ [j$x.Fnn5悆ܶe8d;W2ߴve(!8˵Y8X {NXQ#l:[U7fPպ1+N0qyz0dqʫ2OtrDNEl4-QNgY|CDalCo 3KGSKiP쥳0 7r8 Lr8= ڠZk~ ~z]x^GUttXz/2l1cͺz WL5^YTnE u/-(Ԧ/i|~tc}-uC'%7,Tw2Vߴ;OsA>QtM3?(Ul<Tb}'U:=c~E]-]8ߴzan'}=|/֏r%z5Ar1X.#R"l}#%~`zmfoUtq\zo/U X1c]_tc5t| uUC4/x~&1Id ?pPjÕ.78))%Dą]ʻ@J+0é77Rlʣ0tfxEԛJD}ɲf $9b%*8% 6&1Fly7ٽYZnW*P5!oF19b/Eq,UGY@RTA?$tEXԝܬ7#_]"p[C}>L_@!ڢCtzHPGN6aR+)Ok}&.V!Ul ctml`D{ŀYf&F3 NչR*?_rW^uˎun.|Nc:1,^ WX:^.,eV1@]^A v;@.\mȀ-CscU.o rb q8\fz+bp6~f F5Slt.U 2lE,؀'/8Zg]4j˴qTS0շu+.yQ؎ xhq6o"8֧@qG2.v'6! ʆֵ^qeˌ/M7X1^| :\eGS˗1:ftGao9QWS-)u!yvII$ ~60i.&Eq36a(R"؃]}^<õPU1+5凜@ˈ:SeZp;`"Wdo[@6ԏ+EJ# Kв |w.w:z^.Ռttue:՗z.z]$n`eX *ͳ"J0v]6V{L0_b-<}]ڄxH[I8m7]m97 ^ޣoP:7boS0ln wmr3zݩIvaSxqsX]jB(:\YzCCF;/z\]ַ]zK/Eь]F1~Ռt]Xzeut1*TF:#`i(/!}:ʪRb\<"W^WHvf̺ ψ7+~#C Xu[l>Ic+<[zCD5c29YրP)jr3pUP ,][?ezvB#+ =CA˄1߆豗otq.GGF^tt| c:n:cޙ"J+EHCGVkk0ejnh_ c%VeHJY߄:cb)Bl;w,-r0Qru. BQ8!3n(72A:,fat'A %HFn1gW3aCS{C*7] g2fi:ݳ Sf%S$mj)7#Wݣ>+۴~rhj%@o/ޛioK\e.^tc::1|Wˌc:bqe˗\ ەJCDx) Fԥy37eT+%ˮ6b±^b 6118yLd.~NP6 [nq TVQUqi9,a\CHi `lJMe-/y&f $!g. C/-̉veJ!( # .X*X tu|E֗:^~#+z\\tc.:T|o]LeWV:\c/Gz%:CPPk l&EЖ.]ɅJ\ݘNEGl@#1m-%-LU"3f׉ўxA߁Eu/%"mĥ傪zJ& = P[a":|礵:VN 2.,fOPKfX׸(mK ur"4gm8Y*TD{hb,;) -R)6d孙y"mGbb0 EEb_UhKޗ zՎcF:>:tc::\Xz\]n^yьtej /sdTg*BY+f6Jv9 9L/ddF»ER-F;KMB:9u/d ʝ[SP듙@ ,z^% >h7O^&XGXT?r(+& \t>ku ƃٙT'm9Mޘ#, .l}̒5}I_]'dRMB=,`@O;f< $il&w7{[z_\z871eˌctяюc ⻎/Uc1rP!#@7_.&vf;J􉈕QQ-D2`GD8={LY%Ï&YmЎ 9heC2_Lsa[Fi7Vl+ǜ}rvɱy^%-Wc$[,fGdF M-KKLhZ5UB۟rTPҳ ;JYj4>"Wrp70EK޷-K^XїqRGK__>F1񎏅tm8bFTcĕC@<dXt}I`V(/Bp7!%r 25wqru8ckm5)*N m,`2~X\u|mW9˳}fi/@c05C+uʮ ZZ3 [ef 5Zy3lH9.Nu&.:X!YL02BQE~Dc+OhIϬv n LUgwb{ʚYeT v7sWw"/Cl'Bf|0: .wEȕe#׮F7z2[z^ttusc]ˌ4bČjB JY2ܿWw*|l]vmtG k m= {D]|"#FttͣjY+ۏl"+(b;^AFdgua@\z%r ]Q9Wj{W60c V4VΌ,6DB: Q]LK˽KUGhɺ6!4}yx4 Zv/5a;,X?i)0&ִr;ʜy*L7R2;w/K[+ˌ|t|W.:ttuc*:8юQHĉQ4T5!+VlRy}fh+͐zEŨ^v naLī&\"\KW\`2ݏx\r&&p%p8y#`%BBXqOWXPo.f4{öMyGTLYnq;/=l< Aa[Ϸna#YC\f쇌^H}a~\rWF6G24*m_BDBk18]eoӔk2c|+X[Pj^;l1ftr ][LTKP;ϢdW\߁r;e˗Yqu]>:::-]tĨăQ%J* @峩!8ڢ"LZbYnD M#9Қ1V$(ƻ5XaZj2 / ^Ĥ mPB?Fޓ(bYj%ɴjnr/u&鸌ڍ_9=.'a6NV~5>Qlp `{}E5hZ}e˛ů7:]nofOH7"?BɓV'TI%I u8[u 6N&hy|Z_)8nםǡ.fu3S `8%ζXFAAv#v4RKn_ŧne7w .+yٗmߩg2~zL4ufB qckON㚣!˱lS+XO3r܄ďHW9H&3}]9| uu|A AS[a-GsK}%Txa9W; <~ dq0hğ"bT;kcFpݥc…ޓ>wH^G-5DcR%kB2}7E/agKx|0>i2Ԡ6ez۞G6"a1|귊u~Z+DNB\o?Ʒ.k? EG }E=4bwCK8 ]iX#`/F }t?( #\hԝ2i9MGmI{L8bm'](- \071&'hKYx>#h_Rta\2@SWBU-̈́mM$Z-u#t.ˋ7gc~*fn!G66xxvM7\eƨ*RYm Sە{;p9#P 2!W̛O{4cI,f s ROROsi4?ix Mw@jx;#Am Pkt/f^=[T6w|A6(~'uVdqx^t]i!5WC{wTR:PUpߤzr%h`b̻: :[Uấ.W'PM(1@6;BMTf ם+ۼ {=Օ"At+$԰}cV%'B-o6ӹ=tf#6W+kmppfVyjM^ضDbC~bakO3_N.e:KNٞNnK χYw{)I9TCh* ."vW봻 >76,MջGzyJ(obteKG[ SyP類s GCe,.xv&A(c(QvqBv<7ZW3F ۻ^ c~V]Y7v{3c $SzZq|p waG-ǜ|o";~ ׾ tXps--7MCX$FSkUyԋT}{p.$bVP@P|s??Tʴo6k{:}%RD*˼3#9޲+yL[%nn~m+K7}ЀmkT×=pT*_ 6EO&\W-ks*TtcD-={񶈇a#]ю ηUEdV=>6g nCaA~ QPlaҐwe@_n}ص_tĤ 6mz\'5)1KcLȜ5{tx콧V1o?igJ;v4xnpG"^Xb2n[wг٥!$/\`}r♸K #^fw%CykoVt;:m)^[=%R3A׹<%ՁIty~&5F%C81Ǫ3 d -?'MyspHRYwQdH_[+uV|' E,[=:AaqIMB9|.cq!_6C]Ϲlzkhv;aЙ'[-bЩߴtFJʼng/Aĵ*/vQ%9뼏%tjQ4e纵3ؔ7C+K߅e0^Kȕvf!;ܿ_ 3sNH EMs ;1Nbi,cO ;3<˭m~URn&Vu}(B+?Pۢ˗g3 aCu3cWUǰ\:͠!W|bP&k@ͩuO{ lEtcir&5v'$aaFr7.^rMTW.^._asQ,:e0EW)\+)Lve)T>MR s1;t:c=r˗R ]x7˗a(lIl$\VFe)sT}c%rP Ovl?  }([)oIR~&,ޜœc,$fU9*&C mFPI[69WGchħ?Yo|{WZZMss%6>hq3#|bO,6-y7[6JV;n4Bp/"vX=P:g-Btt @HkG'o'g '~~~~~~~~~~~~~_W5~_' g=SSSSSSSSS/?D),1F~i3b&숐6t~dOaB/s؇6C;>v?l1 q'ql03{Dfݽcf?` CWK1# $!G$ G)GL7q??sl??s</?/?s'? ?OS!?O4%?O?OL?3l;131 Gu<@j;Og|(~&zW~_( :'+?H<1/Xn$ *Big%.ߏA>{MB2!gw7rZߴWOq%/H\P5]UQĬz}ENT‡գ}>ҔXfKz3-5[2-P[Dұun xw/+m2A&w5K|}Z"R+6!us1#D?[??SK|r?ܿ-r?ܿ-|rܷ-|rܷ-rܿ-rܷ-|q?ܷ>?t%^~/??-ϗ3csϗ-srܷ0e??o/??>_q_?ܷ>~?_|D[>$0?-Gλg9$I'L>U?;߬|1I.i %`:= +{ Ev7xݘ\8G4nRƴyUYo%o5Q:2Lu{LBa3:͢ٶyt_܎P-U[6g^γߙ]c((CF!maКx^=e'2)m q^%po<`^*Z(BY.n%*!XcU,ZӚZW hv6Gʚfg"?X ]6ǜ!E8jW) Nh\aW@MYJnb\,X1ib Yz\s.\GEGoXxxՏc\śhtXxfLl::3օR/v]ck͗=^XJ ^-5(9>BjqV uKmR'bC7.lDK,va&Xoݺq(chbǦ"3/T"eYx`byb\oB v܁m&lYPL%"Á{ @a߼p_9n@6/g9IvGd-S]8o7jK&Jߴp!t&;MX^LVsPD><)7v"s/ >{?!Z&#-JLmOv X7 v.Y9zC X05xM]GM;ͦt6..Yr2ˋ\bf.ы_ыEcF^:j)DS9+o nQU@5my *pNzA[FpLd]<,ܠ+lJ6\k׼@XY츈Ldey6 +lEqɘjdp`ؙ!K?ܱ{J[ϛey:Nx!EK-&Hc-9:@ܶ`i~h?d3sxqb_#XgxSRS߈T^vS0}M*Ζ1{qTl>%. jd+x\v&%Ú~s5X9؊. Gycfl -Xa=|9=f. [b)hq#o G4xU+,9V2)u.R%}R$-;rU^Iv%OQv+%t_(=Ph}wKz/.q.brŗG7Ŏ^.u| ƻƬ|7/:.1`4q2 xS&mX% 4t,!dUDPS2m2-XVĹJ)+HX*e2-c3/pʷ12uUoX,p+,[PfϔY\J&9x9zXZ+i]Sqr&.UT(sB=6Ynl 8ah-ݘ.ڰ.g1۪lkH1ʣ=ԏi\6Y-2{]sTB 5}e꼆.,W4C]9#'L>SdQo\_j3.Oχۜ9#o}Ż>au,mflCJgc<"u <B,a\vQ ML\ /&q )BŇ#Rټh/MiqԺ.6/O./V,]._ . oz,cY.>5.c6J>˶\AEx#ٔnAFcfqpʭR$Gm1ùX=LCcNh<lTL+!6*ͩJ\Wj;Z7  A`v XCpy7o؀Xж&\tC02cki}~>\E^zq7W >B)Ft0Iyjܷ\xg(McPY IvZ߫`w?~[7lJ$oPMK%Pw [KA(̤>#cHꏺB*ڟFY!N bbN>.^Ҵ:?/ziX4X:8𮋮/VbŋE㫫/UXnm/W_|.f-Lc(6DrMJGZ8vSEr W7+[ d jpˤQH䘆)#y8-KnGY ݢRfg3t0-js V3*yPfL̬}{U3`bp 齈9mϭqLRl}( 37CTt@65X)񫖋X+3+~X^ l;OVb@ag"x^d< aYo% z;'BqnpD&r8m*g Lch0O,c9G<_0ܵF1ECB耣P;E]E[UՋ鶫K]m/E,sŋ]/GF.豌]9ћb|3h;K +fȧ!nkjjsaf:qX@wS(Rn&t3N_dhGu.y؛Lmf6f;C7YFZ@q>bҢRmǎbfȭ@*'`OR {v}ymDa3CP= 8Éь-շU,8Ovv(΍5,]n`6*Tohzxj.bWEu]/2Ufxjŏutc]b]V9-mn.b˗d7 HŻ)wGvًիb$!nntn]^ Lo]^=K9~hP!%[.y#&#mz0b,,!Vlm_Z^;ġ.KLnP37yxY6yAP1dP&|nzQ*u6ʇ;BVu]6oЛ$Xn#- <*WTsQ 8Єo @6wmĺm, ]nŗ/[t/Ex.XˌctYz../̸c..l]s bπS ە{e).ap*Px(+4_/d'VT ؎ ɛ(=GJDNx׼4bt belf*rEsior2s;c EUR/|^̤Ȝ],ٗe`R38 ̢…{3{"D1A&Mu]tXm/<,E_x}³:...q|K.7X˗Kj;F3==)ZH:a4ogU,¥'M٣{%;v@ۑ47o;Ŷq/QNb |:ɕ8l[r IBdp6 Sm4;WT%8oipͣƆLpC1&lE ҉7~_PtJ<{IH&,c*!ޡiPƣ9ҥEiz\_:ľu/Mŋͼ.]\uF:.5uYq7.^.0Y7ncHUlw]`ѽUCLcul0M2fҜ\.˼!%`[Wh-xD,U9NԹ2@ؼݽG\mFf\R27-*[ǜݘ :rT( 29=Jg_ZCHMX.ٓ/(0VRB+KRg;{G,ƚ.zizy _鶛G1YrWVo.,sŋ.eŋbю > 1/E| .,~!hYCtto"DA+qdj]ˍN;,b 0#n.a^r ډם{Q!YVq^b.`q %؛Xms= )G]մRCY7v< >Ps>Uv:'#=!#X^Ҍ (]=K%I_ݸP+[oҞe}r6lANl =TP B}Nב$ZROM|[,rs^Q]/ı<.U6ε71b뾎GEVGľ-ushF1Uhx...^[ыŎc&axE D(H(˰./=n` \2uEzX+V|C酵u-NZ -b!>_Qy܀&*@fQN0}o~gn{{g܃mYϜ(E߹*`&ƉTؽ8 c=cnP3?}:e[A8ަs! y '2}wGVF/JS`xc`0ފE|!áИ8`w*5_ƺos_7E,Xttb剮:xŭYыYq̸c7.9e򆻶RT]o>+9:*#a6r)EknuDS+91DÓY]C(Q[#ncfQnJP6z;\+EA]1HZ*<9 ="VXRYp'=yv]ZY;`5R,U V&x8~Yo8[áy}X ~:^ABe"~ӽ*/3W/Etxwi 0 ~rI6XT|uG>X:,{ytf9hF8:1qtaG:;u]n.ȑMR$IZ#h_bFl ^F >H-l2 3we٥QEK%7q.?1غﶍG7? /L2 ɍo˛vPQ3}6̸m71]B)/‚F,RS6#U[_Wv̿([WH^Cӫ%+Jnb#i#L70co >\v5eG:>+y~ bWU>'8Ϋ꺱c鶯#7tG1"Dq4BZ( t%OdSχOGyWk!gib(}S[ t>Y 6c/#B9>/C\;Ҭ+d&3;Mȼ-U)0*೛;s}yty7^B/эyQ1 ZsѡwW;1x񼬼&c^*vlJ]^;HOKw ;6o(64ԿVsy>%d9.gN?.+Z\i{3ą y L4u;P! EM!wC>q?&ɑv_їef_ٗeG߶_ٗenV^^߇XvE8Q ? (PB ;?3~׬Xfϟ>N֫J &L>:9q*U*T"EzDɌįD745 Łcut"DT  `UqPP-=Z *7Luu;f׬"ьN^f3%M.Uߤ.t% l=-`:_3% 8)g U"Xr@W<^NْߝNIdSg_Mj>])F´CUoCnlk]nN#E-˷xߦ7#h_Rt3\2pSW*<4o3z[/Db6F&]y?oÇgI ͇dKs|<hⳭrY5T,6  ;rg~M^&j2'5 ՕR:2j"v3#؁p>q~p~'d3vIwoWPP2bXT S4+?h)[^q:&s7ң$H $`U%U`e˻r'KJ_hzئ(CZbp;J/=cMn)m-X=6̻UkjC]DrںOھNHQڿOh6>ao3oxxϬ_௰wziryU+=a .>~nVA`xl@sn +#'-Ӽ `"99 tܳt<:(+muludž q'dNyzKKewX ݧ 9,겏&mDb6 ltEھ )7$.5RyJA7XvǘƹJ$?xj|ܱx+ ;̕,,n|,2<ޗ.{~: {zNҗGx"l*w)` -sI8AϧӾltwΉIQ ecz:Ϭ\(7Q>`tlZh/8 7́c"3_ʣF{؆פ5v='+ELe8s9NPkd{Js dt> V9DlKpU}UWMfr}VGgx}s,@a#8m嘠ЏKg:; :RKψOX۴|@0Ricmȗ4=9[z_fuu ~5[1o-7V\[F[A}37Ueʏ=颌d|GX5B)nfHxq Wxذv)7~8]˛ـ;5Nマ[3`.˛sm,dݼL/b7VϤ/_q6ШBܴW[/+O7ԿAԷ94œU㉬2Wd\$22(VfaG0?j4;u&\rp)Ӑ}cY-ֿ^o,-vH6CML&CZNkRZ 1+( )"߇z#2-g>7Gŵιc:h-_`*{en$=/}8/ߚfqD=*ist~$kaAD"XwF83:rVYO4{ww0c/Ljq)ߤ8fG7et?^a8<]uuz.f#iVmqd{Ѽ9aKҝm=NuֳlqE:NCff,uX\'ϡO3EӘ|W-@m i4jt IJ@ pI#f]vtt(wj׽]CjWV{}pDJR no-|p'*af ffy\v.v _:h1(-K}hCó87{wG¾+ ]>p}T]_xyX1^éPQq0C-#X{} oJI/•<_6aϡ. Kb? U:kq,cU+0 $0_g/'_,q{/Š8bl߻tY=V_*uw0&]X@35Lobu,52K4O!EC ~%pTo.GGEtQ%J@P9FXYu.ؖ|y?Ob<{/ d1/fp*&ƽe[6=I|xBRg{:o0U=% :hvS<<jV]xn"10yYm'DYV`W `խueCp5"Al2>C*]:A>{^Xyz*[U#]3r>C m"#`ycx%5%A o, .=a^T^~ЦB>02nʠL!1`k rԤQOw+S=ir|܍=Vs#ɟ2yK^:9}Iz\SlRJZ?̗kc87 4r$̠\TG67~#7/%:U`(~]CW29~&M{\ BEQsjTTs%U4K=;m:!Dbw^  fp! eMPz#r gb˩~-dV(tljVZh W (6]Izn6 G#.Ezqe.o.,].VSE<)w ~r˗Թr\r /E~L`,1No>(W9۲ox suv7P_%;b"f9gv2,x $=V72*MJJJJσ( :f)/@:ms?Jꨎ|r_x˗/K.\z)r˗/Er.\.\r֋rr.^rOɽ,TY.\Yx/˗n𹵄 ?iķK|O|'> O'|>wO'|?>-O'㿉_|>%O'>9 O'?Έo|>Y,O~'?矉O't7NeڑZwb|> O~'?䟉|> O~'?g|;Q0mDh;_&Ddɑ"Ddɒ$L2'"H J+Tp×f@0;^N^^̞ܾ̾̾Ծk0u_}\w&YgeYwM659e^0@,lZD^f*i:1y͌גJnƽNV{'1¹%yĹ@Īr۲,XGzº;cBKc+`ߛ̙)Yͅ@PpΊЙ&F2VUZEcJ#v$ 0/Z{yDӂ%70S;ql+,Ǧ&[`,d-K1W=!wV(6/Z}Phwݨ`-y^gjXg( Ak-Q=M*ၝaT%XjZs73cwN} S]wԁF6+7(KRzwy 7UoB8t +F,X~3[z^Sxc\U1Wleʎ_'GGF 3ʇ@tQwx]Y6*ٰ7cgy88.4Uo,a E່qך rٗ }^ ~Jb1~T[x8+.f"n`::cD%zGM8Wpv@t {ϽFOEnVWm i2 I;megb&˻q(諷Sm2 fhW=Dp/pɷ,"ۼ'D; };U3߂u']]:j'h)h[;HyW '_sg ģ.Yޑ`t[i[|T91S&,cG0NM˜a-Ն݁^tJ&\L{28>_BcCؐ>.sxWZ~_y^u9J~#+e6Mh9Tߝ>""+lS#!jb,2^5CI y&M| Z_h6z2yzX1o cAi{4M(K'0%H\puVAtw $II*&$/L*UݟrQ]fItfU #3.G~G,R#M+0_49 (YU|EN _Br/'pߢQ-ה8 (.ZOn UDHph5I ym\'pʣ ,Y6Ryk=˞q U"UT!WPnpS-R7WTV695]ғ)m1[nzQvieٳ[㬨pF-to7}!o8 +]\'sʧI˕yM3᭷ ?F 8@U FIgSf]؝@lS7E5;mh9#zTJzܻĻn2oG[Ŏc../:./KzՌaF1ki{nWC W<[A n*~K[\X¡||O.2_RӘX3nc~(+p)ϔӻQmsX?Ak~8s0a)~_ilAx@EaK(*whvr e`)-_1TnQs8Q0s+fp яA7Ҡfs \jGACC%Yz^yZ;~;r]_/[Qt_:::|ՎWbt[]XZӮe7 Oz0vh^u\f%;K`.Qa+=uטHyymQ_t.B U$-|M6 /eQ7(W_^eo)B6ixjB;9HnH`0tՂ5!.,!|..[.k ˗.|L :.tWG[2z: nΕ*UDAH04\veU\)1.x^KN+/ +U;G3}]`:-Smzm$zfd-!"ܙc|\y@uGt59ĸcsv*o;6#SN$nNvh]d$b . jhh~^Y㣣/K_::^z: 1X걏|zd*T1#Y)~8%Y!"*!gk~鍋Ħ;= . aEˇ50% FXMS5s7pZD)֠ ,l?hporv"Ђ@Jjf!t17ƽ:Иg_w/S(y"hj%@{x._MPz\..r7ދF1ьutcn\cՎ/[.\.2]lܭT:$MSHb5|ƥ-SlHi.XԄy.]qsDHϬcc&2ay~KmxG[8_‹5C9䀺 !A'8.VQUqi9,a\(/~N-%I4'9f8;h+{7]yާ6l ̒h5erАD 8"qkK_/F\._ьc.:1*>7z^2㫫.1WMgc xz56"K.%A߁.Iyn'"fb ehWX6Xxזmr=rka+M'&`{> x<)eN*ma9>5^6*b_-7cKrvc(KgNd.=ur߳]^R@Bήt_p>kTrTsDMmCLJA;bf"ze/*4cKXGF1tc] hn:1Ռc.,|../QƼ:2s9E3!I,3%q߻~&@22ox#ma]eyl^Ք>ѼíZ=x ɅcrvQ,%pϫB1Z0̈́8E;b\B5t+3*OYW7koGPzoJq4V\N\ o^ifXC^ṢM:hPb[wC3{vG?C7 ZFY!"귭 RW2eG>T5Wŏьcccc6Gƺ4X1%hxzXU)М-aIy ciM]\@N&X9%ijunj!q ON_Ht~[Bƒ'tRsZۼE,a&[s#6vb:Ȅuzc*Uo,gtc@}4B ǟ_'1Uaؗ Chxw6?p1)}pEE;/#qb 𥃟N1"6_(kqɕgn^hw%Eߩt  = ooeh*:>;]qcG11uc::>9їh>'I#1Q%h Ԅ @9X`XBl–y3k/$ّqҭL)S-f:#S#01öiddv"!#jڧg{Z?r`vBJg elVg|[2 m뜮 ^1 2B`(buMVNFH/o"_"EÐUvR/`3.J]DcҰ yB9g l/nːr.?ϼ{rioYiru%o>nЊ%QYf+~>.Ҽ/[юGzGGV12oHEABա!ҝ؝z-w e(=~/'n'3?s fӷa1)D(ݯW\Jn;¸}Y kV`Y^'5%ɕK0&5dḰ2un ѝOϣNRTC <:=Nw'{;ǭ'{;w3g;wq^sNw'q;NwӼY;NFǯӝ8 (ff">u*S5>"fMI2IYK :_bTbAJZf!+K[sf9wP/yʿFe\?4WQL к,Xxe6cw+zOx62*25ZcRPx?ipKfNj\ 1$y_o#ޯm-_N%1'O" N[K;T.ڃz~TľΪk]KsSq&:AĎh 6 d(&А JLy(NGsQ%EV#/oDe/3^bEJ X?f&ؼoZїQj\cX"DQ4> >d0Y(Z\~/sz9Gs,M̼:ohmg竱=#OnWuJSfCg/l@ |S o2(/ jPQy}@ nS(M.~l;@R=3юI vFJBm,:a|Dn7~;iՓ DlZh|>=tE 9Ejr'czlџļzM 餝^1˘1ˏҮiw"w<13:pT4Ju?m!w/+StEVTr$>Ֆy$72[huM=PK{hUHMtNHU.tg>fqr ^ mL(nx6@X'5Ba0+ bJ{N*le}ȼhx/>9HZt_<8ofaa _Y!Eʗӷa՜#ǔ dԛNkݪtCv_c.TY,# lgS9R*@o ZTc/-veĉá^ c69bYEq=nChXUS0ڋ is/'Mʐ< d TkCSn6Sx%}=NQnlm Yt qެ%_Q$`%!;W]Ź?h{},(]BSnw6&"x<$`Q,Hn`Ra(;aZ"EAЄO |X Oaٝ[:a*_vn Di0W4JrÀ5HDշ3g6KSK(T|h[]lNP Zf l=a871S051~:~R#Φ\V{x>;q0}'R4iȟ1W7{'P iÏq-٪XgAҚ}˛/}#<r/@%"*V,X^pKFDCsz!EOqCQ'%꿝K>n#h|#5{cEtBv]Vu_Uu{K<;.>%> }ىN?$~Ca(y0VHw`sa͓م*b/,zyeJt# 0Z"SY@!{F h2haYlþ{L[~dn<Stʏ3>;7]G0!~H?72Xl:&6l*vb-Jll¬6~klFg6&dJ9I_g ]vOQܱ+Pnʎ~lւ5{R(7=Xz/K)r^ vSAv u&RC;_9 -_8@^41\T&Rod߫0fwaZ^^kܸ [ Cġo>VD2]u|.a,- w (Vҥ_骫6/=j{D;yrگeM1>ҒӉpX]ωEJՀux|~/7~c10#: aBIEk1ҔϜk>nz||#./Rs*,}=YBqb/n_^ tqяTP%B3Qr_9?3r@(+e1Μ~]_}_7NRA1цit#NS\NgkW]GiSX.^F;Cqej>hcǙS޽! =y٬{J03 @\z389a,ћij+5r{[ {.*o:斋idߙ!WuN.~)]ub,4G.+_ M/ʃ)f3=.:K++NЄ)4g?fY~ek[>iF&~7GOdٿJŜ/9(wrNh!\W;.*tֿ6nZ8xo}CN@+6gxEe؝y /Z2uhänWhrb/be]{Cr#u8B,5MwE}n@bb~t왧:(.sV|J0J/GXv)]-ш͋4FMX+[ބͯӦ^y >W^ {ap.iau(oK9 z}):pL +}mHC|~{z2NCBʻ` jn }f>t"Yzh#o-FNsowt e6ZyЀR KI8oҟ!><-U̻;7]BpԱ<! zRlapjz0;_OGv2[yh$dya]ydMMg4\zH/0%Ll goj{U'fEf`w!)6'cc Džwmq͔cU3-'ze'8>>:F9`+m;1a /v4 +K/^ |TirroT&GK.\.oI5M(ApR|7+-AMANf3n\/r˗.\e.\p/`2qEt>"W!C{$S瞗B..*lIj0N\%ik/K3=g;D˗7t5_ \;oh^(*3;]O2wg;G;;ﱑA;ﳾ;ﳾ;; };Ӿ}'4$"eCW0NMCY6Ngywg}wgywgywigqwi}ywi}qʗwi}ywieC)uk;>ӼN;ӾN;>ӾN;ӾN;}w;Nw=d';Nw}ĝd{';Hd{'s;OONwfw!AIC.氅҄w+Zֵw|$*k'vH5O.Ϥ?QLiQtOhV>~bxcP_紱-LݾICM~;JUhhKeg]H#|}腐e>:Bo<+uhY4J^&jxX"/KF}(0f[k dZ?Hc(E}7.*^WrdCTMAM!j.K,e,E_tVlCWICb9 F>ܷ/sq~[[?o[?o[?[?o[o|KAܼ-ϗ>_~[/g/[?oasr_/q~|o|ԿZ}I|2H`~[ %vrI$N?"|?"7wXb]*KuLzA'W,ooV(.1Ƹpy=hҥh3]rK&qk2td"օJ3fuEmm hIV[511#m -⽝f+2;PQ26B#40q0 czO ehRK  @4y]zTQ]7)R'7 7:KTB-TY5<<@,Ѱ:moyR5S7)MtiIf"0E8~Qm,m9I[C-K q1pՋR8ޝи:¯(72ŷu,Xtc@Uh[b\siExыbM]6cuX豎̘K0ttg ;; ^09.z  a[jPr|G-f.6ڤ*N]2n\ ?'YJ L  "%;uP& ŏLEf_A38eEC,ʳ`Ĺބi YLز:JEVW*-X찾҅TT6s3[ICJ9"Jf8o7jK&Jߴp%T" ueZUcǘ%&b҄UvUJCʑX*h4|y¤άmsI0m( ˖KX05xM]GM;ͦt6..Yr2ˋ\bf.ы_ыEcF^:j)DS9+o nQU@5my *pNzA[FpLd]<,ܠ+lJ6\k׼@XY츈Ldey6 +lEqɘjdp`ؙ!K?ܱ{J[ϛey:J.38zM'2xa'+b*^weEmE!(]X'oh6sW ꭉA' g|$.aEl%ŋJz21@&A9_it\[rY9Ŀ.7xwbwM..\YztzXeb,]\Wkjrͼlc 3L'.c- N (U8(mm6y{p RJBEPKkQ53&,صh{LK`K􅂮f])ަ38p; y'^.L%XFߜ}W.:l% a{E2n㗃HTıŵؘ"u =3mZSbQvw˔آQ>YAmU* a;#ugǁĹeŖ@@zm7Ku/y/zxt~±bubV.uph8xcj͉qtcGvTq]j.[a̠p 6#3UoZ !$Ew)y X/_kל(b3ؼxS/֋ veT7i>iо¹k"yp7U`8}rѾ9L5qͭ Av⺋跶GG mV.7ƺ,^,XEM㈺^]W:1c84s7cŗfьvzV#O\C|Sv6=g"Ͱu3SvbQEMg䜿T=J] A70 (fazl8vn倴4|9Dŕrw9 ͑Z0UV N,C;%|8E*lXm* /f-|gٔ[m_%eHb9,E?zԽr VZZ8_2tp9YT;}2C \y֗YzRXs1Z9=l\*>X5H V?YMŖeH[ޭc Vv9efa0▖D%ܭO\{%hzxj.bWEu]/2Ufxjŏutc]b]V9-mn.b˗d7 HŻ)wGvًիb$!nntn]^ Lo]^=K9~hP!%[.y#&#mz0b,,!Vlm_Z^;ġ.KLnP37yxY6yAP1dP&|nzQ-28ws8W%yC}ڋa{F6Mw z$d(C f¼۶ M73QgEr X$n+o˥Yn!wYsz6 !d7xg׭)pZ_Ta}Ɔ mB`QGDMy!e/3={Ej:VW\ s.,XˋGt\x/x,13*$*㨣;^de˘yy\ #%J.Ǵ::ķ Օv#fJQ&E/CXe*:\A[+3\-g>QDUTKß8kפs)2'rf%K6ec!Ԍl(i:A˿^Sl[A$A.7&3ԥѼh{1GU< E_ 1bʹ].^w7<+7orwWıbsxŋ7\qtcc3܂ʕDcFI[-~[UR*Prݚ7\[d Z3Lx[[gܲn]E.0 ìPs&Ż=")JSn6{CCxK=bSv 0H+\hnq!t19kcpinx6=vki; IĂ1oP4cQR鴽./cWw_ޗ_XŋbfE..:\E\躬ś˗/U,ҷU\H1t$uFض;Zhު1Z&K3 uNL.jc0K-d"M*uhj\ ^Btޣt F.$MkLBZõ 4f^"*[ǜݘ :rT( 29F vl^rqG8+*0{/<^\}}'RB*t]6K>nM9"˕yqc,]GF1tc.,]]]6c^ph\g1Yz.Yqe KFR } & A^Ӌ#W(.\jvv1g;1wuoV 0nNCܪ ʵUÌr.0ZK60Ht U[e3 doewUvA3=fwC!~ "Z#zB͚:cU"[Aov?F*BP;NCdY/5TM -g60H*MoAkH"a9vzU,_k>5/z.tXbŋGF.X7Z՛|ˌ]3x㙶[3:h1àO} L4`{7mfd+i JLsTS >Q5LE41r1YG H* jWl:&@HL)n)UUmY{A.7&>ÙZ0a0~Zʝќ@ELC5,Y݃vȑ][ψG!Pum^)<ť<* Sh{eGz|t&kلL/_."eq?Ⱥ1ǂJ:,bR%,]UՎX^_\X1M|..4PY$HČu flP F9svR(V#d(^Ȏ kh` MV›9fZLu?j6Q|^#D]CufjZլE^h)Q1w2lgB1wF3[/2|{G> fzB֩9E# TB]qaEG NӶR%~G9Qv;E(A@tD3LTѵ0a3-Qndhq;/r͂´,LL% o; uK)j _t::8U1Oo7h:1ˣX]E8<̉dH4cP CP_A ;q.W^YlࣰYI)3MF7]AUS9 Wln}+B-N@ۼuRQx;e۔we>9wCpxD!zVs,)5ڱxJVKf+1+FO3pk4$A'1^1,[ۋFI9{%"r:ɕN{8>ƺ@e܅kBo0gZ2բo:=`pAW$z2mQZw:iEu\X._& \ʏ.7bGE}/1ю>'3m1hF1<5HGG}9Y1D+@A@@mILv0qYfz}eǦ}B+k˳J8JoiyʌT 57~\23GLǦ*m^bB>;ĥ?K0[^牔T6fϬZΖ (;Rp. r`ĭU~y^W,F]ՠFų!#ܢOr*a};~YqzA"[ַJN8>(}@LCs5_䞆+0l] 鴲wX}_F@^4\v5eG:>+y~ bWU>'8Ϋ꺱c鶯#7tG1"Dq4B n6&[l cLoSi˱ t4LS@(\O AG|zAo؝. >.=nP(_TD {R=S  ;Ja b{p z2;ʜ[Θ$d"^]a{]`Qޛ1 ./}/zEp7ɧ{Dc~PZLgKAsЀϝ* $.G}7𺮉 ,ch²/ʹtw.u|$H#J@AAil=V?wklW.%>)r_2"kUn@,mfnO#z+t@Hqsѿ,>ay}#FW3\*Ӛ[tA<ȸFo%<[rnWCaQ)­ê&gnt uqZ=tLD9*ـ,Ϡli7el6dǐ/RWH}a;ƣo2{ZHTyF(+#*,yv5Mhw E>|Q039d|;j_uubz^/WU./>8]X.ьeh$#7u@@Y`KZCq!3Vؔ.F7;sl"w~rGޝa KxS^`BxƜ+sWFY~>1Z]n77 _Ư鱕N:3sk/<mF1P0ݠ_y{$g`@Xvn'&K:Fj}/c̘}k׽rOsU9;+Fmk#{_m.)pJ@lkfbJ32[,CDhX\Yrm=% >ndl9c ?9G?s\Ws\W?s?<O?gY$k?O'Iy?O'DQ?Dq?Oim?Oi4]?OSw?g3LC?/ڴc;JH"FTt3S@Jl{A6EVqFp}}gJ{Ɔ 5H.:~0QZ[ax ݷ1u6%sIc$ۃ1 \K0&Kbq}a++xǬMUn,{1{9~vsWTPVçik뤼5}n;z?xu:e䋛Y2OޡCV>&InMXh%d nvV:D>4%0[p؞i>*]GSjsY<甧tpwbhOأ5:.s?["@O'_ :atF/wecTDK`ܠS~jR.'$dzqST)(ڣQV4Cy9B P,K 3/N;ý R-+ г旦% x\bRLL@Qn<yfX@ZLۮO?" X)~RBY+K62VGx\葄|!)*+M7aa-\..[Hw;6Q+nY8kh-9D՞S /V-0e*α)w\=eD %gxtb+ܻOM/PBW|IUV%2M2u>l6FW?4|!^eTOJm$fPb,On5}UO£&5uԅz=tPtCh ZJwbyBkUf#B\5n;W/Β쪾;nIг_.},t%ch8pt>nSWD3^&4*c(MdNsX y@2;ί ŸR$vփv 6`YICBL~}3Ҏj.qs=!=]@>{pOVw|!>iE?mssl S?FxU@s*(.tHU>ڊ]1$M\ +_ݗGxP;7歩0hͱy4昫\=SUXS)ggK>!@n;A.RKHdc v!IW1a5K#e/~wbM:>i-BNR7rp> nk-3gY/a( %W,˼ݹGK_ ]{O8+qu&Έ+Y8cZwp{</Ǥ `.z<&ow8U>7M_t.?DS Api][#9>*3rҽ ib^qJvf4:`fH%/!hWdϦ _ emgq ͬ ./{ ^O?VG*ryO㌫CE2ŗ~p}>e&R/~ |g }eՆ"kK[ԏt#0U[;}:Eh<ᝌG,ޱyVg[ovG5`" @rPn%!#P؁Fjƴ.>IMLݎe`L wil]sQ){"Ω9Ԇ>>D\-owso1ҥD*:T{u/ɰ xlK0ҎxwKO5&7oϮdM4k~GN#JyÊv ̓f CN=J3.wN{wҁv{읩#>LƻLTz|N*9r޻#͛(k㔸 a kwq0.˼kU"nLa =<*`&Ě :u Sq>oJ{VC0 }(:MKQeGgu*NpǦ}!gVJXe=tH>d4W(^!6sҮNB9&h9r y7-Z>.0~;θ(L#鯨ӱ»'q=ܗF*)H_pN efjF]Ǔ,6mיr+8Lfi$N@3XZtH5tc+:Gc 1 5 +-Rx> |?R`jCvn^@B*Dz:ZȩR麥tD/B}PAvQ"x&+Z+Kn|^n\Zr7bZ̠aCypgrIO#>=']|N +em \1ƗFUmj1[}[e?*Xu*]!ECʦCCwԖ`70`.sc-̔#Q%k no reus`Y)! {N@/ЀdIC"Dt@p=7 cINr0O 2$pn hQ {;P7Ssx7LvuwN-Ip.C}u/VM idP*x}f0zg>˛.)+ i#Ўe2\ˁ FvGJ,]./\i8`"A,Nh}F\R'sg=v&y,iq8ַ&J1.\CD72 *ҵ&b)CeHD-ݺs FKX/lO\xTeZ.^ P} V`nK8n)hC{WW=&3PTa6C2!To=e+aÚrq1T//"W+eˋ.\Ydqe\K.\?Seg,k?syw?s\W?s<GMs?s_?sG;{qSq \G?f 7~hOdOsy<O?gyo?s9<K?YiK?a40yXvbgY$I?O'Y$a?O'DQ?OSite?OSim?Oi4]?OS)Tu?\S_tK QU?(hQu9e^0@,lZD^f*i:1y͌גJnƽNV{'1¹%yĹ@Īr۲,XGzº;cBKc+`ߛ̙)Yͅ@PpΊЙ&F2VUZEcJ#v$ 0/Z{yDӂ%70S;ql+,Ǧ&[`,d-K1W=!wV(6/Z}Phwݨ`-y^gjXg( Ak-Q=M*ၝaT%XjZs73cwN} S]wԁF6+7(KRzwy 7UoB8t +F,X~3[z^Sxc\U1Wleʎ_'GGF 3ʇ@tQwx]Y6*ٰ7cgy88.4Uo,a E່qך rٗ }^"#t[u1- =7zpƢrk(!8˵YhNI ~)*Mqf#Jv# P{xKw`_HqAHc9zc'!F@uC *nf]m8K\SUӊk˩ 15# 9 V/^|W[ׁezޮtuXGE/1ۮetCP[e嘈F_TXRڛH˂}Jl"-fMQL Kv.ƒ݆cuXr1UA73?L&}dA_.o J?} ^/Q~c7-ŗ$-}!YŒ]vy­qw#'nfCW3z[y:L6Ŀ۠HX@721AA#Vћ~2$.%cznaWbdd c+}ȶm(+;J*&$7o1wÒ}Q@󷓫}MbꏿXzEXCɋu(3)gppaGʥᗛ~w\]~A9;"֬TY6VQ ށ R+ck:2W[(W|˽\Kz.q.9u:߁X2z\|,]_czX4hcv^8vn5y1s"7 ͎Te5ʫՊ *Mϔ^ҥs^9Eǔb?oy-%6|w-N/kPV,(ˑdbԟHf iv"U.Rw]N Vפ Gb8&UġO6t qA/Bp*WoZz.cŗ.26߉b,Yz1,cqLZ.^1u#D \[O@|}%'Ԓo+J ]n9ه K L#bu{Ʒ/;/*YAV[`{C׋q. Mhn rL؈Qp]4xVwz n3N'OhR<* 1/B4;ܻޫzܺGV1՗|tXSV^ceb֝t)j~Wg##Gc'N6Ϩ([ythCIY3ElHmwj}R!m'+o'uT P 1{/zaIB,݈yO4MP6JݷIGu"GvE%MUάAqe! qtt^Z߁tt__].\F1t]^eoV1uc:߁/WKKttR$M :A輅S*yIpV\q^Yx^1]"92/> ܭ 7-aynHkti'kՌ3!m8ՎgZB<`(1>hJPEϢJy. +1<: 1PK544c Kc/[,qї/ ]/GWE_,uezuXz>2D*V@`єYzJX5tSvXZ`0Ú\X.>snǼk[9̲>nr]bnR@7n]T-丕gjJ=y k/Y?K} x8}XY#gt$t+B*[m<ޛioK\e.^tc::1|Wˌc:bqe˗\ ەJCDx) Fԥy37eT+%ˮ6b±^b'+[ Xyi44LoXpڸ NbT ݲj5[ZSsJ3hNr1Ig#pw Wn9JOm>dIZ*TzʟHͥp4LM2n@S*z؅epoY/yz\Z|'Kї/exKutcˎeJ^KcoUd3Bhbj*aMȺ˹0P`7K^[ Ȩv3Z=y U1;@Q]c H@!@o|j_?r]dK)$,DH&̅z##fJ '5r}&kPl#TRń[op!PP/VΜv\{c]BUgKk-3"zTe}FQZ 6:tc::\Xz\]n^yьtej /sdTg*BY+f6Jv9 9L/ddAH tۏY[, S)M$r0 e@rL@C ro7'KH";>A}l!v .{q\&~Rzʹ\^߈c}V":|U!źtO[zK0в R@ov}i sq!*ow~(k(BkpkM?*Xxn緂귥 [qoKc˗GF>7qˣ XKw.^K1c2CG:nx⠍FkB]ٻ,;Hs fz==WuQx7;.eWZ=;Kʻ97g}hw = Sp5fcIhzJb9h=^<7/kv&;̩&KoX`lk-s "ʏ7Ug^]aNfbh}=-ĭ5⼒ݽxA쎡LogK*nn>ە3~6D)b[K.H6E@yQw`P?B]JMS0Б=j/[޷-K^XїqRGK__>F1񎏅tm8bFTcĕC@<j~'(Iʱ,*eNf$NA; b&H2A+hEMY?PCYbCֆAlM_(XHqƛ)HyXvߴubPWH nqPJ,\Lc˜Pjp>p(v3&9ChQx+*;l;a1v>v cgX6%/hu;jꗢOUB=Pt+~k˥uq5<:rIQPz'ìl[cz-U2k5X`Xz5 ѕܨ[teŌcՌtXF\aWG$H$eDR9 q;g2Rp?3)1<(F2}fQ͈١G#Ƈ?2҆:}v)҄8cegz V垿ߤtP1?ni뜮 ^1 2B`(buMVNFH/o"_"EÐUvR/hVfQQr} sz0٬t= nz*Ćvv(.)w?Fn3g *cN!Tfjmߩ{CqyoZ(6x_{$Cw//K_u^zތte>WV:>+՗WF:::hGWG~(;~a|}?3矙x|?>ς~O }G%%0qB09Uw6M؂ך% [8vܻO$(KzDy:;\J:"{LxPOi|DKV%Bqݩsa9@a7RZ}Ŗ w"as~%K4%u(୦UWh4S`m9cb5ķ#k=W=%ګ<&f⠴;Ǚ<ч ]*ϲTX`ĭzfJ WC3국v3p.'/1MܕLDAo|O55?|G>S#c?b]ePdԯ$)\ lY5(\ -Y|>/χg3|>_/ϗg3Ῑ|w>94Ϛ~g?3矙'[ H\2$H LD J*Çt]]]Zrzrz~ׁ߉݉߉ߘ&&i=}}}}n8뺏q]s&#m4MNU=䎌J&kP @*%GMlL7+MmA0>ǨX!8ʅ2dqw^xY MDW/J!'U(PnW%KVf;!: iiJci-{.e(;2Jm4!=%is\ف;6g3_WEVOsuG%>}I*zWLgCŽw(N 7U}U\ģ'Wz1cjv\eVR[+-rc[Ŋ>KB o"&M0D"sv!}t~)y.\~ҥ~ >7 3ĭر8`Fh|WX%BA+J*Cbdj&N!' ߧݱʛ!& F^,&zs|ޯX'! QIcWog~N ۿQ.-Lnxaʂ=XW; rf{׺E@9׀=.~\} Z8֧̀'B@ $d&As;y$Vf1ݫ[B B_pX)u?z3{AHIblnao(x%1hG0vuM[X(é9* 2; +Ss]\ gI[Zϛ"]q*C޷YީO[ɴ #gש-Qm3guPU1<u'+pk QM;iji=؃ IZVR)(nxؖU17%/WH?( yo]"7igI4˽ҳǯK8uh=cS[EtV[Ro2Urwt,/A3M! NŹO[5)CsZVy>%okJ~xd| /hE_#'uҦDެS JU&%idG-aF+-`}Y\YyoX۠9] p(=ftۤ^aP6Jf)QChCW2ȌÊh}e15c0!ؗ^}|qv:C@tF`,4ݸ v(O[Aei%26j%J0~h <Oi cGѭ:%:Zwqw6n>.{N}O7IWKT ^w*W䘥J-]BE2/+_ƫ p"&~xcO[YU (82DY̥[\A`}m %vw#Ł)=?/jz<h2n~h5*ވP]fM 5#̾in2*'}}N/DM[;z:Go;I[JȣY%uj@ v0>8o=g&]Ol*d6in>D* -KWĶΙJI{@=HOPpZg> 35yv*8P,a='#vY+SP· )Tq"v 2Jv`yyO+tJM j>9 0/]Ti%ςم1cKOP-OE0l}HWaAte XǷQ@s3 R|o.M5CکT 8QyBF@DY{Y[01nuS?RA.$&iQId\MR7?qīoFwY|m<>rქd~/_uvYx9ka*= B"ڐAhW6f3aQeeAِ6$ڹ8ضG1W!=d[ 9$7>C'v*븷>GvT[zOe˩tw~QEZty'HbӁO,\N'tt%d\K=-^xixlǙ)Slg:R>(Gtnq\O7߾1dYE\Rx*7/,q۝NuU\[1@FzBN~`aD:e>bSr}nG')(Մf4yg? :X]Y+j6yt($U 5XJk}Gy~ :k/{>}ZfUҏ6Wwk(Jy2D`̴3w˃rWL8Ķ~g ~fWqmXw*S^b]@`n:&UiKڻ>D~_v.=#QYCCLW=/B :F f)~ږh4H6,>emJ2?{*Jpí[C~`'k oԊe*-$^ޙKکɔFSb K8qw&72{}e%LAD_C{]"o1r#_"0Bo" wM0#%z K%{0k*z|~X^&I_Zq~TQη)G>T4R]yA^p?csigT~3 W1;> S#ո~ev[ S]n](N6>(r:t:P:C;_Y#QFB7=&톯 d!șO{;K9 ~`n? l!GO>Wh6 w^v*$:_-BҒ`L' 13\;$vA۴kSgQbUQ|\>$ȬPeP۩DʧhR9ΠKCbst玸U(yXRrGDs&j/ͫ}%WJm?Yڏf;} 04YtoVDd%rgмDɕd@>Z<]ߕ"~[Z|t 2dX6ʼ>;)ʆ|>F3J`A0j0jFR2jS곒_]x-Ӳ /g=`e7 >s `x|.ȨDݕ s3n_tZi|GIuN͠.ӪSCL3"w6^loa(Q>{DJ3γI=`g/ȌO`)wq3Qa}O A_IHZ&poAz83K.?|q,ؔUp݂1]e.^pCYb_uØȐ~ߍ1kW.l`o.O g*%e:ܱ·~*c9ϸD(=zR7 a&AV!*/ ft􉉸4ii9{!XIDy -2x7v>'Cr+5"K ->  5 -t.%4 q[bA%%ು"zO7vx\v\ex( h"<aϓ96//1_o&'w8%9T([4;x]U5,G{jڠ:GcɌ~'s;$  'eNaZuM#X[XgitW>of)oX;eݍ;z*ߘ^fP͍VtEt{L'& ״uDrʎ!bݕۤ[-,}J-n\ +V8oZ:G%ǃvAnVw?RGJXp,Vzv.=g lӼ-: H߾JWنE.WA>̿ ޸6 ݨdTJ6[:_jC_t7t #U/X_ +r׊1QFkqs/T1W̚*E]3SKd7/{ wӤqԮ=fcjw*j_(lV۞R Gs@t::J/90hs.\eo/]Ϥt;V⪶T_k;KȭʲM{*(BTQSKCpL巽Ѱ[9yބ7KϝMVwo)m`y;@\~!ʏM4VH\;7ݤW[ȨJ:Ꞩ!?0]/:>}'6޾&Vٜ͢ AzP9Bv[u9] 3 C [h<Fs˲s+vݗmљvsgV, a~@!ǝ"ջ0t_S*V+,sxtz gZۿd*;qlɣ.c>XۉV* \/Z-Pz3MXx.^] WAS> /KF`(3DBEE0&XK.\>GR3+)Lve)T>MRs1t0s^ǩz84~+.\.,N=.\riG1]l:2耧&5m=&4[j:H4EmuYz\C???X^^Diz"XI>_ϗ9_ϗ>_ǗTiAPp:#t 6_͗e>͗a;MH-" %{]s*Q26p ?CPd?P?CPT?j8ao(PU FյpX UQ!B X7NWX !*rյqJ=bDɳjn۷yÏz+TV$Hc‡Tr eJ*Y^;$ |aEQEWH$7Mnssۜޜoƍhѫrjկ^gPKWj;Og|(~&zW~_( :'+?H<1/Xn$ *Big%.ߏA>{MB2!gw7rZߴWOq%/H\P5]UQĬz}ENT‡գ}>ҔXfKz3-5[2-P[Dұun xw/+m2A&w5K|}Z"R+6!us1#D?[??SK|r?ܿ-r?ܿ-|rܷ-|rܷ-rܿ-rܷ-|q?ܷ>?t%^~/??-ϗ3csϗ-srܷ0e??o/??>_q_?ܷ>~?_|D[>$0?-Gλg9$I'L>U?;߬|1I.i %`:= +{ Ev7xݘ\8G4nRƴyUYo%o5Q:2Lu{LBa3:͢ٶyt_܎P-U[6g^γߙ]c((CF!maКx^=e'2)m q^%po<`^*Z(BY.n%*!XcU,ZӚZW hv6Gʚfg"?X ]6ǜ!E8jW) Nh\aW@MYJnb\,X1ib Yz\s.\GEGoXxxՏc\śhtXxfLl::3օR/v]ck͗=^XJ ^-5(9>BjqV uKmR'bC7.lDK,va&Xoݺq(chbǦ"3/T"eYx`byb\oB v܁m&lYPL%"ÁوN{>On[t=ܳX/7 %Jh.ǔpNE.VXڮSzx)-g CNҏl=b "֧Ltƕ-qAnHVUV:Wħ>>ho<:)T}!=%Ō$h<vJJ6a\)0We#`EḾ |nq6F $L )͋_R%rև/1p}* f M6ͥRüo< X\XtX|+Gĺ4cie豎5fĸ1ۣ M*8.Qrl5-hn0fPnU*K"aQ[ec 1;]IQ2^ p ,{@6 *1sy]b(fyj,ޣ0X,XX+!܃=a %b )$. ^"1v27Y:JL]E ϐI Q] N*!kG/߼anTp{eksa;O+W sp2 &@w Ql@UIb;7Èo"[nĶzxkpP%+ms8'I9;cɹ׻/` lKz-9렀Z |┛ҽ [ׅl 1G>Pr] ne͜F`~=HJ=k$6Z1AH _zQYmqmlK)V&͸k04ⅉݱ7]͎GY#|GR#j> j=c zGuKomWV.<ڬ].o/tY,X΋.ttc1tphFoE/Klj;ͣ,&GO"[Ql{M,E`f73aNģ=K %,9{%:bo@`Q3ͷ٘p ehiǀsJK+s9"`!=KX -}g*޳q B0'F0Vi[ eJiL"<كQbʉ6Z?^X&1e]WK tY/c]]F?XmUeŋl[.0C1nyJ]Ebj؇I&[3pS.WiN_(T.A z˸HɈd;@^K-'Ub.[0u7֧&׎(c˼R'$ۀmT ^swd}͞|T ow 762.Jb rЃ՝mG~M&.VB6O)ʫ7U*TH 4!0&:/[Ľ!|E.E2[e/Km7t:./t^> s.,XˋGt\x/x,13*$*㨣;^de˘yy\ #%J.Ǵ::ķ Օv#fJQ&E/CXe*:\A[+3\-g>QDUTKß8kפs)2'rf%K6ec!Ԍl(i:OD(w&$IP.]oM1GU< E_ 1bʹ].^w7<+7orwWıbsxŋ7\qtcc3܂ʕDcFI[-~[UR*Prݚ7\[d Z3Lx[[gܲn]E.0 ìPs&Ż=")JSn6{CCxK=bSv 0H+\hnq!t19kcpinv`ǝ&?DJm(\kP4.Źy1zCXJMq| GE,]7.6/tYq,]Ee,\zdޕF1c1#5VűuFU 1հ6PY[WJrapTW._Qm]U!hT#E;R WbvKZ1q&n͙ 27:A2 o+J- EBЅ9*gcyxnb;[a(0=ANz Йin&4 S5aeP h.E7Q|me^uY΋.1.GGG1/F84X.3,E,%)eѾA iőYt.5;;L^vAXix 7j'^v!UFTmdxeZ9y ! zsdؘK2nHf2G]մRCY7v< >Ps>Qi9`'nc/wr|wwiw[[( #*jwUc`IR@spyQ_8KrЩTWІ,l<_ h Ȉ荈 ī8h@N2t=s{׺v]|A$l|5_ƺos_7E,Xttb剮:xŭYыYq̸c7.9eCbUJ(& xh9ye:ZpG'xDi3WiTwu]~Kx]]1u^Yqt].GGWWF,]Vm/F/.,ucՎ\&g> (,M$bF:TS#a)hjR߫]MdG l K0dSmM3-w&:56UE9D^6{T]6 hJȇ+"M.t>o2%MuA¹qsQ꧜?yX9jdۆC4hYYp*M~&0 凃  >eѺ^*VJbhQqf~e? o[`Kϓ:OMj.\Wzy z߆鿁͵_}/mE']7Ď4c|,t.y fD$H1!/ L8G+h,6spQooȠ*) 6R7y>`P XK'3m1hF1<5HGG}9Y1D+@A@@mK-[,YeV>-j SA+s=n(;,$qviTgR@ -G0dD{',*tW!D8}w)O3X匙v|dvPQ38W]jb}c]m7c\ m>`D]2}MƖ@z̼;)wV/Y[ ";~uJVUv э>=z7#0~K ȟ\yG~%;a0(jrU]RnY f0\v5eG:>+y~ bWU>'8Ϋ꺱c鶯#7tG1"Dq4B  .7 (o-ؐ''pDpm#% ֡bJT1g,Zn lʋ^^]Nu&c<.H` If.6 u؇5&X{)E`6"#>ږȨrV7xWT =G +}Sm[MzcghQ|qitz?a)Bz=9?G;zF V C=͝쉀(?΋p΄2fh?j S. SRy C^zAȒwS&7]xV._ ˎtXG·61:"Dc *A ᣇS4L \~*zqe+m 1vq;Aj%e!αXı ^.5o%#f7#~% CA4 uD2>qRR\zOneҬ7pF-48켊lf}!K9uuHIOH=чckONUG`.YMOxxy,N[=aؑ֕`zrp/R.t]\Ʒ0t\hŎc+GWGE HèTFAwigMDǵnN~pS>uD=DbF !BEl(l~b<{67q&"育c @oNDh`rFg#qV#x`i;C<~b\q7jQ /Qɿ/[tʈ'cA<X"XzKt] ;.^_`ܹz+G=L;*WqPW,w ve&3:iqMGmI{L8bm'])- \071&'m.^ Ϭj'Tl =Tʋy~lF#l$e7Xklד;6eŗKǟ0 p%@սM(fɼEժ?ʄT;yuE^e-nH-x!Pפ*Sr{ٙhfc+9a檎"v3#؃>3cwi?0K Lyt^݋az3"HSEyGtCӲƴ1کiI{X\WGLN7FX/x_h[Xs7ң$H $$JY{}l`fIh1srGC)Q2ns÷*,a@4?hS/hױ*`Nըnol@Ww?Eܛ{0oS G]4[b- T mWy / vGc[ʖk@\fˇ#L ?-K˨^饜Ĺ\,:Ƈt֕~ԕxp}ɟ2؝+G kE?G aُ (N BQi4Y& NsǮY7e,LFżm]e|Rn ܡw9RѺôH<5t.7ūPP卫i\ adiacsIh8鴸ǂE}4:ĸuYDZ~Ԭd;s$O􍶃{rΕ֝!6AavY_]IIm;Ɲ_G5v57ߵJժ K܋ZQne2 ǣj㏓&er̔#q/ ՌYK.oMUfPQ ;Xo03Fo0k/juMȄA6A:A;h o_L.@au< wҭ񛺹# *0BRUW4U]EQBZ? EX0R^=3֡^2G Z_SO; xl r30h/iͳjccNQm-Z6-X!f`/wmS?2?LyT5CcGOs=N]O`nzcV!ws0p"=k9 ~͖s.^EiSi-'tDwʽ28Æގ ]ƍ:)g'K.ݱЎ y% dK-ȵXi\|ncޚ(qlc*~bɻٛ0Q"<1_;"Wؤ;/hjv3.of{>/VRћw\Chf#&N`} Ee˗oI7=~@[z`i59#tx7`qW&Pʉ]p\pcUУoEܻ0{~(NlYak2Q<"j`^- |*R%%ҩOT(sXvg&oyn>HԘ,XXtWCwVudw߯N|ו:eN@yw^=Yt;~w20a(PEgCq弓_*xl `sM鏚<%ƪuVpcNoKJfqL2WB3޴&XJZ{/Š8blz;لiXA +2 ^29޲Bml2>O#]:B~{^Xyz:[U#]3r>C m"#`ycKcAԕ@*Gǔ,Amz{=FQفX+ufU`&Pztd*D}Z`DUEⅆrCGdx*^m4 W8zܨ-_ `Z>EE%_Hʙng.~'~CE9L#Dhvp:P g <&YbхJ_쭶At?,X2[ܬ]äKps͉|u]C4nnWi.6")qk!Aii73UƼL|<<Q"x&+Z+F2NMԜ$;T[,\\cޗ.\b.oApKa'F 6/7pyKVar vfݧ- w9%:6D굌)gkً>`y).\z.\"w:f ~!K3tQg'.VK)i)UT_|  F0<ߙ|qCZXAQ9~b!sWY9wb,˔-\VgjT s1U.\#NIs)N0hKz%"f7j˗,b:B>o863V#J cURG&kaWRQ,X.,qc3`R*h`̮SPwS~žemnFz!:|.8J=C;P*Yr\\X\x4S)0֬e|'6e4`bp2k+JF YM5r&0̩&X4˕,/iAJZ[ ,} -% ]Za.krRVVVVVVVRRVVVRRRV&WJt))zyVBeetk[5Bk)˥M4뷎,|7 ˤAaa.1,.,]^˗.,qeˋ,.,Krˇ7 x3Y|_aeXem _9?նԛ4)!pvI7vd?ml盯^Ƿ9$-?h3T6Y<5`(ʚLAͦ|-Lm$\i{Wr5\{z2oo^s--gKX]y1,/%bϧ&]u:#ldVZŷ4m`rMޙ:W= 5ֹ̈!肌:-sѽN"JR_Y[Y颓I~ͽ|_yt֧nw쎥 i,rBՑWm h1TŴo_4ޗ%#߭}6wMź|"VTU["_fޟ|%s>8uQ4)M$J?6e#}b=7c (u S-dڳ> }@_N&mJ\ o {f/\؊;gd+PLEsՕ +l|SnML_ bGۿOX-?]!7yu[gZ7yEZmӥ\R>#/ė|?%2_~r>=ܾm?b_-_ h_(&5yF\ =ch9 OͶߏL˯i&I$/~<ɧrKM/^6?]qomxqfD">l5G\V%I=:%OmiTePȺO>.UbY>B4gn(mV'mWGڽk|^PWqtAFg4[{%nEF|C& K,{n.XybK_$]~OBS헺^-=^MGҘi$J+@'T!~= {eɹ.X22GsQ/7Fssgq|k+~ =IgwS%H=c#w3~SZol@XX]]K'?R뭛[ݳu7M/~n< ؾ\t@=.`1VjߐiMFs=ɴ锐uޅ[kkq}|Ѐp.]$LҒ[|w~헄7PBZ}8n^}^XkkR~Zxh`6TMh4?ILom~s[")X7vkz)Y!\t!h tA&x "h4ۭyz6;*.E6"pOZfM[EQӪK_;2C]XQ)eGތKZ1YkKF,]ߩzGfBm6HmJyR3"QA Ew؟}~>s\7x-q3Ϸ>dwjvmÈ+6d^`(}LXG%2sRqZv-"d2dSy)I)}ߔ\6J-̀l ![%]M2ɲmrv+7 SɉҹfږS9ӽ23y,:SZ4d&z@Ed<0 cl sEdH& D/*s~G9otﻎuz7ϷI_j!m;߿6׹u'I5Eww?z)іK-&{m}I3ϷϚMN鬓I ܫ\M^p_= 'zA}w($ھM?t{oo)}CKSS+?3 %ݶ[76ooiU][wMor4 +ݬ̪}tf?ۓO8E=NuVͦMߞ؟]rLww%fO6q)9L!5k3[6ٗ7%46|tիk۟ϺƘSOYk{,{C-۶ٽ0ޖ_KF)o:Waw-E#eof؟ۧ>{ ^}fYm;wO7:;{^{nkwh,["߆ۛl'pMmRJzmMej6yEKcI;i>oE[S{$/m/]mnkwi/[[~l5>Mס)^r?IjM{l}K~][շ*i'kW[׿o~jn?L}>^b~ڕR }riMU.7/5u!4ltMݽgL4<+%%Bv?$ɦ._m&޻~ۏ~)/eg+^ཱྀ\& RsK,67oS9i RS{zm|o/?eӨ#wȮǔ OoI%ޛSvo~f])/omi}:$?*P6kܡol6 O/Mo5#M!ҮYgg髦En4emNM-spiڴv96~f_ ]R?؆7uPy>4>:ӱߵL!{YO %lfo3H?IM mܧvqOI,FK)X~.6qʓ%_a% ~Q'"A_/i3_,/gv kbOz3ޙ]$4թGp@X>a8(cLQ搴ba+Q;|~;cnu{#VWuÛxSf@Awr[ٙ?[kBo PF. $iot[?_'`cў7^oۘO"xZTEW( 1/hQbڍKm{~ht ]-8e-Vb_^\@+!_jh=?7=eY? ]-wKmL 7^qKlh4{::cciR2ƿ>mZo%ɶƁPI+a[ ޭmeKmf7ng&h;wip%g:Rv S+]I4gu溅0ioi73-B8r$پJyj/շg'ݷݿoϱ}W8Om@u&6y#q3\9I\+ݾhM,rO;}:㵛4,yװI O6?M|wO<&MG~oH;<:ȕ[W?I24%`ܶwW޷Lo׺KKYR?r^GhK X3;Whp+{$TdgξH$ՖqmM;X?wfN l`Mui"9Azq:;z(LK2FVHVszhRgߪog*mgj)5۝i#B)Z|%\udm$FˋƏrhtwIf[74 #}u3M}1n9dH40UdEȅ٨7_ cE\Ϗ tM#hJ~uaͧlmwXz{7M(CTY%F~ O17o<Ӊmᒄ69B١-"?J%3reBt ۨg941lXo O|ytMopy֍hQ}V|sϾ=H/@,$gq%)|;}5Iy̗ߜOw/{O혗KW;hI%unQ/WOuvNC9i%S4m2ZI;/-iܾi)enSK׍Wi|mx%^wF'Ht;]խ;|;nIROnNIv}/U/w}/_i)..4&˼XO - Z𵌶H5U4{Q_=v$׷v]4Q&|osW3vm$ˢvhX&{2ߴSм;ev׋Oe';Q颥4;͟I:H5 #^s}u&b}" otۿ[KK~u|NY+ bf&5u2[ K?? 6~gQtmQh ӾPW1-k]XG9:e 2ělٚo륫>4ae-t7;dmMl(VԤpq}Řrm&%&KE{2)<$A ]ɫl^@N+_76M9& F`'LyoY^70"n:̒l~lSlN=4g۶@~CtWTņ {wY$hʣDuTm)̼ -ȁKG,18%f`ýg[;s6/#4f]žL2 -1k-e~@;"SgB|mc~(/>d8<SݧCM>o=">m2pbn;lE!Ű+h$~A6a~a7{c, lH# X3tH0fu t @-kcWOU}vR-f"]]($4'S.!Q|?pV| I_{_Q/kR֮8h Va~a*][R?+ l;COŃUy>zűe׹~]n,ɸIm/I?mlAwyJkkb|l6mmMm5{s\{% ;=BJk;*s]:ٟOkqkw}i}{m'Wkܹ?imwm/~/Ap_ {hN4c zMeMM?~M'5j}$Ԑ /{omo~o~7/}ъ[ΰuubj=iٶ'GϦ'w|6Y|N?] Ϋ<,Nמ]=??ȷ2?y_EzyԽ[zmCYZ ?f{AXN/O ?K멗o]i鮭ߨ[;ϧi~-ڣmmJWZ^/}߾mwmJZzIߺ6ڛOrHϯEgoQ?rGfmi;gK?My$HM7o=og?xhbw3 tݶ~I2iŋ?gI~~7}o+~[zcz.<-/o@&K,/z?;llMf_mԿ}ޮf_K}=n"EݧߙmzKni_@o=oFWugsy:$[jky$ B~y[xv}Mqhnw}Hcݖa =!J[}ua6Ky\6m}6Mf߯g٭&b0Exv,CemM/I9_f{۽6e}lBoߖUњ4c5r~d?K$P[muހG|gδկo߶gt]BZ>[IȿAw=ayH=!h%}Fn1ސyQo|2궣l;^s;ެni!{32XE4IY{%-kyPܺݸL"AeV.4^߶}1VQ*}`xHdIٻODv7q5YSHX#TWOUrRnK*@]1Zփ۠&>"Z-M;Vw6[*$ٞ,d"ؾOY_m.oeQm7S9YWwI[_6RK)h(U 6Jꗓ|wOi0^Rux_$ЋMI )+m~ooI%4ZuK>{RGD}A*$0%VrZWRM&|]yc 5w1Gb P$6oioZmY mﺷo[_N#[a1I ^sH<pf`gf o$~>f@NlNf/y<{Bm:Oe\KQ{7V|eL%v7w-UǷ*&}m9}Եܗ#/=@VpZ9I.>vI5e[Nv$陓x)[#]k|ȁ2^\ ePM_=]L7kpdL[Y/%eM Ye!jj !XW3H']-I:rAdsi["7i~-޺o#|z6>@P2IQ?߈M+k?_d }`-?Py7hi}?s}fOҸ L\>YP6)|D0gr }9-[2]w=7\~u~>_}U::\-sR2 LNIw_=N~R^@3% lf%>'>kI]c[nKv7ؓڹIZMm̺֒dN}lw/oJY[TcU_g v.ׁщVF]v{gmk@NےTۭm]6KK;]KZu {˼zM/I>%V+%( (m pv2.TRnm!2v|T}mfm?jݷ?5ݽAuwM>ntfnIu0Cx㽿F"'fǺݦn2襇-+$5I̽w/.<{]eIzh`M)Hm1,< i,FĦ_v{oI@Dؾ~|HH6$?v&~?Eo_B5&<\(Ld6M6ߧ$ngq3ov|7´,LKL,tvYNYS|[_1&,6fjϭM=.K]9 NY3khoi9!,8E2-ђ@o ` +yj[} k'Z-O,˞Y7実oƹ3MqGL2>{)"6 4j3& [3]`yɕ)Qt7Nד P !$ ۡnM1v ꠲F\T7 ( .yx b}$\Sգ{5hZ{GG(9SR[4DG[׽=2Km A1sƒ}-稐8qTxBHsg./+rܤ礉 ~<6W *N6oƥ#.ixTGה]J&"pJb{yKlbW8&L t$miXXPdjq_J+wM2;Z܎u'5*{>Z 9*fXvq8o@8rݽMj CSMJ)P,Lb-H{7L aϟl^Mo]MQ3;ۉ)s1jGå0HZAesIBg VIh` 06p|g4X06AhBl¯q>w|?ӝN^ǯs|lI$!ӿ_-om{__}=CTXw}mmoe$x߶_y<|ٝo?~w-]ʹʅϱlo)au_t]EF|rOm3mowA=}R4*5;r؟ho>WljfJoj[oe~fie_qv^/CNjʧNgS4pCꏸigͩa{ml_^}.Otw[io[fnKߣmKg-g+QB8?mzrSOkMZk}li4xﵟo,2+[!9I;mmG o_ߍi k~m~ub<~m>8]]6VY~imy_鷾g .n6ӿ{?Cs wƷyv{r-m̲}y4n޶u/zޯPzVVqo@tYyF4Kﶓfd_%7O}2ef?vnku0VX*L_Amҕ,䖤׿4[ҫ}o^v>u{|Mn3Z?\DTMi}٧/}dNYO3r^7RMfD}p{d#5κ}=vu:ڜ)imߒLoqbi/o߿--_ _i}'o _|6pkJaW}I݅u'7 >Kޏo[?vh3|w/e77ٗF_}:nk?|?͛m~HQm>iebޒۻ~6Wg#[O[ѯ3V沾Dy}bwh9&9 I6m?k4^~\B:z۝~_m*}NQe(1H$CةP 2]@FMW> vojMGk}?ikIؽ94ʔ9 Zצr{t[d}oNWYM_aw寭5F|5ē ѻZy.I]2[w\}ѯZG'kxjmx+lR7sIj~lIf@& AOwT}oYՙ>9e}[߬o=%wodzz"Gp߯J$kUIUvV?Ē@-%"m,lѾYE!$ab[Bi g_'{eK\>~ݺ7r[ڭ?׶H-Oް|SDTb[{z'I_D8תq bi藯̗4%-="Q $+$I1&n󯯘>wz!7pYMs*Q}~_hM?}9vS0;i$Îhq4UAEn'3bLrAh?pb! D}ɒK@a|lJNބ2h㥠,RC_8 x.8T5+|kRK+r\3r7._r98G<|x_ Xyf/r 2en*5~Ƣ%rEE:4J$A_m+"Nd4ui\ [SC=錼 Zfmz].}Ju4Ҙס]:>n/ޢ6.`@KN*E K4\Tf[[tPv.XUZZ:ey1t۞)pFfUT3Yyƍ`,KTFY%L(VO:dh4\h eןSC Ӻos}0ez> y*?(g c9֨ZuVfǜZET} U_>hzAjr.eJT\1¡ƥJ*WJ|+̮\osxx'Z|n\[㈷._7f B.rk 1U]eࢠW Lp "sKeDy+1f}./y#$ n!y9!߰jS{= NL{m/I<" #t)nk뗭E@{͊޵(0{ U+ʟ_h?\l;vcx UrpK+=بfOrEccl ]hr95[FۮV0oWL0^qZqVt圶tw_T)y42 ׈.pp+Rp+J3+rW/狎'/|.-._ b8GN j J9K01yڧ F.diX0Rqq*\t13Y#Mcq̜ OM`Y_Useru@9i*.Rnc lZ5_>hA7CµFv^Ue>Qq>ATR"jz5fsQ֫[ct';U/e{"=@7P'k ubSS(BضuII]v.oc&('7d+:evkP3yZ냽ʬ>u [Im\\kq݀9@uՌAJQy T@.T|*\3K̯1/|*8_\~烘¸9q`a烟p"ƫ"4u߀nx3(p*qcKW+ ^`|$Z`Up*q겉׮GKΪ;*K"Lt@[5ވ;D }kWHiL+1)ֺ'9)m9v2ǮW%m7~1w~$/*1Ju 厗jS?t*޺Һ_ћcY]jk}-oz|0 ۟[zA|9o?PĿJ pcJ¸|+q.|Wĵドq̹~<+Rqn-#xWp6ˆbU|  Tmxlú="ּ0kŨ7tHVj!,QpR iJRx kAPHu*W+xj8^**W Ƹ57q\[_ |W._'38eq.uPILT1 Ep5 y\y{ˆPA9a&41 ArTQ RTP3 +URdT|/gŞ*_ ¼5T q1.W|xoq Z.\~ h97 5/NlMB-xg^ R c^5/Ƹ3oqZΰFl xSxR8q_㉟ JW W \/\+prQ8-G>|'žˋ\ܾ!Pq_RHƒO8: s|TqGTb EBف|N!5ۗEE oC_QQ釾շ͹t9;5{#dM iR\U5pxoC2W DQ NxW> Ÿ\s._18q ox._\)R(m*yLq¶Z C:pT3e/b\DܫzFl:;U,ʲ85@tCQnF^ 55LF0;bP*fMg:m.UJyؙ @%U&~ `}Y_t\)M#/J<+^ ~ 3*\1/J9 \xܾ+^89<=e˷70:Z|jW+˕/~ 7|.cZԮ|89%Ke߁|N&p\._~ ׇ6F5Nd1ps fPB+F'T=H)A: ~d Bl_8z@kԸ 5'*W8( 5<# ȘcT_&=J$qV{z\fҀVTIgU" :Fbְ0JYuq mߔpHmtRZ/|.p&|w828rpsq/ヘKľ|7-q9s#< KRW " &( *.,*YKA\Lu˗U\<''60,S0i竹 4KcP0^Gkߦ4a7C뮉). :/bFibh[$κǕU0dU607B60-zԁ$}; ՘riy:2GH ;'X.dO]%ח(n84sЋLzEbc򞟒!S&(0 !EPV&tsB@XͧG^P0VXլ ~Q5ODkx &4xN<93<۪Mu|S4rdm qq.\ss ~"ʼ4;U;+ rh}D=">Lh!ʺʙbU5. M\B*cO\m 5r/j)oʍ:YsSw (n4A]uvʻG΍9UC*o2#UJnFX[CȳcG(#h%oRK6U:/|0Y}!{PL~Fҟ3<̰]^n!RGIBRJd[9}~TCpį޼cr8狏8 W<\pq/oハ9ĩ\._~ BPz_1 ?$kZ\8k |:I`չLB'$)@u3*1pu7=eNכ6X.F] m׈]`Xf-d>%)7;X8e\8qՍS툝fy^XgZߕ-D.R=GE:t:qhT⎮BTr>/Հl2u;V&5kHcWe|JwR&qH/czQK=43mZhApC(\kGfT&BB!pj8~3í":ouK8G Oܹ\ qmƩ.ƎzIS+D|@a>y\/ :HRX)`/WzF}` &*PF.q-a_/P*~ӅJH>iM[ "2Պu?9o%BY^D-y8T[\_5Ix7QzDWuu 7P"8w  #M5.=O_3>, l3%Xe9/h3> (~wӂ-z*Xz?,@6uGbn:mK]2JׯÒ$`NCHރԬTay?X:Q{|e% iOa/3EW忱b&tFJ}+Ǚ r'Hvq=L@ʣx8\kuh ˯C*5>o/r5r,ʻ&8lw*6|j}6^5>gqk\/Rs]ӢJ]o8K:5%Hi- UծZɅGNfq$ ̗x-C*) bmV{4t0C r}h q$ySOx-cHZlyh;s2Ǒ)8ǹ,K;>RM!WAl%<ޙMT4r:Zmz)-vv0[{JjMqQyDT ־.<L)}8D!|ik=&آ'H5/?K r\/6WM{s|Ke^y ,1LBK˅y'u'fh/J-s3|#=)9V9CT &6ۉsIFjƯ)ID;[r$V?l>I_[;*0ZN (55;gwyrl/H+8T:nJᦓ𙬒ϥ}"w7f-J/7.n;KbV%L=/e |3ڿ,_wMY.3|ף.(J;nWkvF9b֜AXjz5+4Qk~6}|/p'q0 e3yB<@ p J3VU'&jP{tſӕ9Tʘx3zV Y.n/}M{Pq.9%ˎe2 *WTB]ߧIr1D 9Ǡ()毬2TW./$Ѝ`i# |riQaH.rUA.^."6K}3V=%R[2O./:E$uB`5K8ZI~+᭨ZDyǚ}#<_/[k=Xb@Jmu6sd6ktӬe&<1r"x_U3"(`t>n^5s+/Uf;Z:pZZ:tVzeRdKWauQohg,cY-@µ׫-Jps44؈\ 66M# u -E¥s qpqc\+ʎ^w~3ZLqMFM4~**TIX|4U ɃB@iLWTi+rJiRJ%%e%S0a\uE9@9@/`&"/~g 'JhėSHyDfI\U/S}u[GYw~|˦# Tz'+\ɱ4#u&M4p3G5S(|9ˋrM8QXmQXn6x:jtXžU%`|t8b&i+o_|ea+JRJeMfٮj4F78 xUf39cƜ4 M0iʕ¸kK\NY1.10%j Q*LDC'N%tX|lhԹqਔp (G<@K+roX (4j#N[,i>Y?>4;#Tjy޷ʫN|\GuwGqwGywGqwGywӸwwNwӼt;y;#;#oaoanaoᾆonoῆnỆnỆooFoB<ݣHhF~7s?Aܸ.-|D`~`[ o+~g_{?2o$0u~a ?_޿1{\k?j޿0~ao5W+g_?4[?0̯?O=?1bS?JF!rdAj -a9@<p879s4&:Lg%/VJރLkHN<1О6 H`?~<'җ`yxR@fv4*ϑ4 ͦ|V^*QUi FoOΙk=ElRLX"]jyb-))\[MǡJ؍0H(KhkhY9%~5kXVhk/ZkX"+Cɕrש0Z9+moЮ[i7 z@wGLLLsU|rm),xHjpΚgxH4)b}BtC8un_ً: =8@=D^0S~֛r8ySO,N|/9Z%KվmWQG-ftma\:9#2T[}-iDtȹ:#ܪ eħN=Tn,JI^_30JΆH]^qXVyGME=1Kok VW`ݯf]>fOȌQA @| ẉ}*>:;RtKvLi Vִr7 }#YڱU^+CA x؜0vS<| <xA @Cb)Zi5@5]>IOX{@baoE :򫱕7zzE\}=DeX\7Iu(^mVN@(^ڃSV"aAMT:9p~@Pz P.ڎQhz:d~g4.צ1)IԾ|(gDGOX 9QQXX=E[Ur(mv4ͫKtϡmly8mS! 3J5O(a瑼FJ:nWϽrG 3jm! G\0`^.ħՐ'B|$z`u̮jKX 6uWR. aSK2Jq=^­~UFV:.pش^gw<2 Olu~EQRe}Ӧ^w]BWmt,X95f EXStaJQ.XE^Vj:f/ڇPoR * Gq\l3 JT=@? 4NcvK`>? xxB pо)\Pz͐a?`q|@ P<<m#ɿ00FJSZ%z 0=}^֠; E}]nM.ɁM#Ā#+q#Z=/, 6SA&;ۘߋ  fC~&:3ZV`6AǐmWzku֢F:$S0n% Bj% )b:"a\ȇIkw}&i50?0-<`৉9:c{@DK%4O.[| Ykд[tz [D H-npV}T> *QA\tkA,&Ub|Ppl89 Dyƞ1_K e^Z0ydd[o&~ ? ߱DmGX`cdGlB C"LAzx1A%}Q b%@1Lj:CcY qpiquGYK +Hi4c>_Z޶}=*:w:c:dѵ5 gzLƲ\^"*Y/wubRRfku9?=͟g|(BzDyEv[i\G˩ Tv.Z f4VꢍKW b g>ZK)JaN6{[4>2m荒h{P[V>]H: (Z16yJ|eEg-8 ˀ==??󄇀L{W͊lcP.5}co>0[5/]:yt[VS8֦`%\λ9˅e oQƈ$Mt+rxEգd癁]s4 Sjy:CWPu&e,szLvƞF/Ekj&(i_:N|  :14s9yWɡD`8BE l C+]=հ:LdXzl}XATnSW4ujK@MXт <+z3r֛peiY \V l Mzh0 JyYgXO'FY/G1h SW(fz̰\#bsQb rV[G8܏"5٬6`]MV55Jq$4rG}\Sž8zK??40JjvfCM_ixn^XbNzPr+!vy81Vt(okcDqN- 5CߜU֢(w κE˶Ꮧ%tA6s@/,*0ڟŤxe3r]^K,zwGw\Tuf'}UlO 6ڈa_(R+qNEaVŀ"sJsR_?ǁ?Cg{zM BZbb\@:%E*{Aj!ՌzrMǩMhR,=FQCڮTp_19U"レXt)יΰdN+Qm9vոCfuZ-` +fRc-4>3blmT,  ZTWOj=a)9,!=ߡs@\ #7\ }m[hv ۴b~϶ヤG-s WEȍs y犁le{hLX~a ƅJ=`.oLߋR*PD/`y5H77^?AX{JSG2L/$E88!X&H!AHĖʘl BkQY7펟x5 8.'1)X6ˡnaM\AMŜ V}hy_9,tdz#'lX)Arpe~u<s-+GKmV.|Hzt1#P\HϔSp d=!Pe=XOZ)џ~xU[';'#Ks_76.%_Xk؀%Y?#47y =J=!MG˷.Y^Ɣ>ڻp'@W(DΗHW ~aw{Q)Kо~O B 9uCW Q֙/tTk:cf]=&J3l8(+rADA毖Ro*{O|dyyji>'|L6M{E(gvXzk*XGQZ8W@s:4"Ճg 1ɱ > >?Z?| |eA+F8Z"4߃uЎ#Į4k)RQ~خmrDyBGz8^Kʀ泸}`rg۹Q:}ꠕ?`V#7UȝWէ*zI)brZ ,BG6:t \Ym_ES0l1iӠ%*u’^&4il GiCC|9 //J~ptߵ|#5SlF\Qjwv=h1r9T8jW hal|G|O _Y_:'*[J˃yc˄U|i>tsC HRe@ZnF*70zApp1v_NLԠXd9#4=Nry\f4dT@:? VT{E Q|MCxe[i*T6&@Fi!A+*vw%jth[{0r"V_bƵ& 8ưCT78nzEKjkGNXkZO gJġGG c]*zumGPnˇ><,cY-@µ׫ۊ3N3HMI\ 68HŠ¢ jIaWjdN(_BVVQ+pТuCcs$5(QrEQNP.SDMDZ1 8~e;~e;~aBy~`ėQ$[Z=?^p'_]w]vGYwq9ty]wJ\zXn\ Wni$eˀ5e_Jb23NoD'_aL\ukݜ <]u}ᕄl[ש yo+чY RTAO Y50H6uW@K`F gPz] Fy."U,ީ ׈.pp+Rp+J3+rW/狎'/|.-._ b8GN j J9K01yڧ F.diX0Rqq*\t13Y#Mcq~,ˌZm5$Ue| .sL.N׽׬%XԉDr~a(w]3%dvKS`'F1PtrKP\i*i:uu<转mh EW0!Di/31-30 |*fQ/-T3.T1D_r؍tcQԚl0e,/yZEmoPRXŠPkpNz_F ]\ermAqbdH[Tw=ςQ&:X"|¾NaFnE޴Qa9YZh{"zJ|=L14*M-1GY-`hxRrU_9LcctaC^Jhh^u!7U+z! c]*cVekɦMo1~Bg4@7} 1t5l|J^n?,h\##7Aɡϓ09bR1E(G<&ىbL0\n^[=_ b_T1ƥJ\UĿ¸>+|WZ\qĸ\ ^q̸<+Re1j*U66ak^5aRc qps$s+5a@]\E\\иqЎ_+نYW%t6+J3* G:XB4x8J~ ^8׊J¥q7|/\p><¸_˗ DqKT-SLBD\ kždAWA NpXg Af4eEvj\b4T1 sJT-Ù#r._ xn9:gJ¥p ps5+\LK2+^8r\q1ǁ ._so+ 3roM8As,Pn!YׂiׅK)L=`i_3#-A.Rx p R>+R_W _^;Rx/RqZ|)\N%{\jaA(LGԳCbNcgsJ7/psW¼u \+\G._ 9\x7q̹|<.__ \~p\H:,Y0n  k5 PαP !zk @YeTq:j nhzE[5LBzBWjk"H?ʂ+̽eQ[]vRRi,ƥC JTxn.¥x ?|._~Y Y}Z嘕4cU/AJ}d-Ed۫q6 ][޺g@?~4n:Ɏqhy&+w %_JA;P?xTr֥K_ |/\/?̮8ܹ\q/\K88q/ \Nx~_q0UKJfJ gq1 1r7~ ̮.c|s+!QqsE 8r\/.T^4lt Ǜ Bb᭴rKJS(Ius9ɻQ9"OVQqo/hNܘrUBCڶeJ+@F]"ucBs /VxҀ sFa슬fe`Ž::@w N< ͎w qH0C3[S9,=5K?~c%({: =4a6~;>}`DrS!x T#4/\5|&%Wq.87oD7_ 8.9x9x^J-J4?-v`K;?W2Xd{/6âH Cơk*p"<4:G}Zb0N%-RU@uLrp,8[kVn.:GTeaR~OXPj<yۗG<_ɲغ[۵ SXDf(uh>G%>[CJ˗350z9qUE_|Ûe.ppSu*Y¿֡ fbW_?^ q1Ĺ|KsǂWq[Pq+.88*r TDpId\hִ"/ɨ6 `UU.X[ G DJ }m4ۮNeY@5_AzMA1s`(=DGB#:7yo/ϑ!GX%,RNwlPQET.dEE:aXYe2We8TDZm Z% zNp=͔MMMMMMMMMMMMm&m& 6i63i63cIIIIII II$Gle6miiiѦmm"\a6albi6o6o6gll'6k6kڲeq|N89+˕/FYNDgRHRVW:QwF^Pt0)f/&V_lt@9oxdr9DžW /C oP:6]o)&at)Dǚ#s9*oZEZyG5|DO"CwBYNry-ʚi_6yJg]xsxHF7p_NR'|' Р+[5= }%e㣅GO2~/&Mӂw2ӫ DV؀24G%o3NZq._>5G2\GX-prv z>};g\@;q 93<}y Vߤi|Sʜ>Lf}hJ:yNM!"؊\1l 2/c'\\3/Rx+%!)]#,!TN^?#=?2_9)oYSoo _-/K 1^f#1-+ |riLZ 9TJ}3i@U|oV+*'(6y˛r@ζeM^S*Dr:il'5 lS\ B[*.tqTٱ*e/5*׹y]^QW/#+0H-Kn;Mk7JSKVAW| x(A4Əb\"(gkQX 4DXXE)ۧ8 kpq.a`iה0?ĭ0xZ?IL/ND Z5q\\qW r_b% [Hy=e+KJ*!J@Ixn\Vi%> t_~ej5ܳXTArpKp O;1樣9+hxuMu`kٸ@0P\6<jq[qt~QϘ|vDw~:-0>̝q9;YM.D4Չ:R%cNX+˗/JkYZh "wX#+"x֭ր/' nk곴TXAJ%UƲRB<:ͩ(%˗.-n_ߎ/K|q.8.T*WRn{~":S6h6`6`6`6d6l6Nd6Nd6d6Nl6Nd6l -l(_- 3;̓;̓;͓;ͳ;̓;ͣ "-Z+Ep@n+RynmlwX#{˼;GywywywGywyw'yw'ywywywywywGywGywgxI֍ȑAݵ]-]M;ͳ;ͳsiHGKe:3b˻˛,b(;+ wGywGywGywGywgywGywGywgywgyw'yw'ywgyw_ywgyw'ywgyw'ywgywyGywGywGywywywGywGywyyw'yw'ygyw_y_y_yw'yw'yw'y'ywgywgywxh6`6` 𕷱 %˗Źqo#rސo}_?Jk1'k _~_yW~ ~??_?z?d/@ ?0/~e̷?Wq?:_iT4/ {" PU kyͳ-1d?u.,TIzVcYnS<<<<<<<<<<)YH_O$O$pܹr\/=ez-[Y~ehZ/ĿĿ%-K-~e̷?1G?2_o[%[%?JڣXQp?J<[x/ڿ1p<ѧ8[O8)YyO,8 /~Q@?pD ᎄA8gBC)&> ?vN!|p% M@-q#O3X`8t@? c؏/2^G@:\`8> &p =/S8@=D_\ aԡ#Ԝ0/C0pƀ \SBAf`9oن[Yt/9RK鉹 c/x0ʱOQ|Lr>鋬mwVN@O=0,Rt|Ez:̈b"ޞ9ӹ.5̎y= g! G\0`^ d.٦ڪy˵] E ɍ>[`z ֲȮ Q]>[Isaz1`6u]- `+FhHL)7J``g 3gVY朩\r"m^cO[ isyԠ)BF7 )֏Zz%ugLjהNSkD7Dz1 -3 er EҮnkj ΓM|D:ZWP;KputN\6)esw!f!Z)/:ih%sզzˍ W R^bMZˮEN|}I/6y@< xht 0/P<`xH8b ŝ^bά%9Eڲ!9'C܀T޹XطAvVC J('[ҀQħa;U S{3F~Y3TyC_1ϫo;o`yFYVK3E-}NdJywDS=G =yFgh'@G5&Y~sբ'SIt[pijCe~B\'S=`bʽzWY<~]\[#sʱw~<, 5b֙Eux:/>q܋e46?@p ?Aѫ%4E4 ? < A^̖ U]*]0wsH7go#v8Q-&^W?==?0-<`q5ZUgJCvGn2lk=Dx0o`#ǔce`Q %_>N(E+~kϗƐ2YydKoRp-\ڦ.ggJt+f0nh#2)+"3UwI]:rZ"=@F]d<?=8,^]!,p/Ɏӎq-.WJe&#C `Z^t[MWSw`\oE=/XYFlcTѤqШhu#$ȥoMe 塍hGwIŬR(ia5&;aLrUk1L+tfd!ղt]_ feU#|ƻ"76Ug1}]aM>Xv}HJ:>|c}园c軜ݡ &*S̝@q)YpKлɝ0"kR"E%eFہ[Du:`/x=fx XlnuмrPTlj4 " ,tyөRf[3r E |ǭkvtRXSs!lQuwt]^Z::ƺ/B%ߴAV YH+ٓ"hMeMbmk؛^&߱~h5/\&W^ΑH|Ы'D΃X6/b')Bj/o>hX4v !&۱+ R_ t@fv‚Ĥ@`'%Pi6%~{I5 8 kt%6_&~п򆫫{!;ifײ)9/qKZbn*h 06=g/bl|s3җ۬*FzwnB6=@ ѴKVF:Q y-X>*tݛ&DZ6=Mbl{c؛~&߱6=Mbmo؛>&߱6Mbmo؛~&߱6Mbmo؛~&߱6Mbm{g؏O؛~į6GCJ{o؛~&߱6Mb=bl{c؛#&DZ6=Mbl{c؛&DZ6=Mbmo؛~z~#&߱6Mbmm؛nv&۱6}6]˴ze'rq7?-*zLPGT$|F'/7 v~U.ztwqk{ gMX l\@ _#׬JʐT5X̰T$$4Go u ^K6zBܻ^iQ,Ѭ9Rhyĩk@*)`>K̗'$Ri=Mp+5ܨ@b ==՚u7ݛX}(XFKfi8T[ eD9HK-:*iClظc7 <UwW퉪D>w"9?ᧉô< u,x :pG _ ' 0V Q^ ʷ &`ß8((fb&#=_x &^pRۖ=ߴ6p4vK8 =[S٭uPD|>< zN O<{kACy@V}iUs<_hՙ9nl"`T:מĮ[fM'X 8e?IR OL1J`$rggSšf$4)0.5?E>!PH&V*DXbGmZ %PZ5_ t;vvL[=َz t~Ώ#pg>8_px a0SB9F[%/pmDOJ6%ANntoYl[-DžҀzkctUE9Q5<^: `C3(еHMԳ#*i}xLE6$'X|1~^:b!^M/3ziPPdp)5P(熫"&q1|Һ-z-@rv7Y:4識[mV}4M.Bj_jujլ{CKJ bozcWȄxw?0bQPfQҮ+:,ep Ad@ί'B%3}YП΄QY~*R0K]} aZ<+X3?u#`׽JH I:YEN\!Gu3 Zv8y' bNR ykSRuv&c2"KtX߲);A-xc_ďn0$WMloYpK4^ 5 LrrIl4&he*h*$7L53inGX~Ӣwa ?}:=iUxT6D~Ati7+hHa8-t תz4yj|Ȫ̟)ҟiTFӗ+>X* <ſBȇc MpCa3:kKQwt_^f[*S˗.hOLz9b1 4Cg|{イx}S~KI&߰L)O$95x[Mt%tjkʅ^M3XWhM'>%p#(,?XrP0V?4J 1.EqnMǸ&˃;(5QA4έ .40:8M@MLqՓ`Ks؟5rئK` |B0c r@E^)qufm[ϗi"iԺB(0@0KrT..2'),K*V&&Ϲ6ɵMrl{kܛ&ǹ6=ɱMrm{kܛ^&׹6ɵMrm{cܛ  x΂-EɋYJ rl{oܛ~&Ϲ6}ɳMrleܛ~!r"fy!?H6J 8r[Qtɜ/8\e9 ݞH/Mrlg܀%ܛ~w&˹6}ɳMrmgܛ~&Ϲ6ɷMrlgܛ>&߹6ɷMrmoܛ~&߹6"wꍿrmoܛ~"XX]Kt!hPcVK5eP"ad-(FAoܛ~&߹6=ɱMrm{kܛ^&׹6ɷMrm{kܛ^&׹6ɵMrm{cܛ^&׹6ɵMrm{kܛ^&׹6ɵMrmoܛ~&ù6}ɳMyMrlgܛ>&ϼ&Ϲ6}ɳMy6]ɶMy6˼wnmmͷyMy6Ӽwnal;͇y6B&~e\O&D"JYMq{Ů$b-7̚PkQn]CD  9ơS7bB772( plcTv\P-"@) 3&em]DC~*IrѮ1 r fT8ԨfW Rex+1¿¥prCWxxn_1ド~*8\ `t_GubQRr F3Ӆ9"i GY fA#5tb@mE,WKruM]"v7?-p?* f5T;]7".jd cd_;Q~`;$Ear;娱4\U^4lG9BzsDimc+~Lmt:Y5Bhb׼klw$A62=hVl>T{1'jiTl}Fi=P$Um~]3+RUTRpς.W2Ŀ~ rc Z/9ᆣ|7`B+5^fb[~fa_)s\dYwfT/Hny'gG(Z fٰJyFLyTU 2Z&#&WhNy ML],stwzפaA]u= g^w a*Ȗl_f+B jWC#z6]ᅵzT}/h`uۤ˵ ,׹W$͹K뎳D(ni(˵rW4֪cev6[_cjk$_&nڵt}/hy4d yNNYtm:'+ϞbSV_:@4nf_Sˡ 8*%*WW+CjT\ K \+qs~%Ke9Zˋqn_C2+\3 hjSkCfX.-A00rG2PD,UXeʍ  jW+g\*WRw/\+QTRT5/ ߂\W rw9(#sJb R(ˁxS̈* \5:  5Tpy5 Ƙl K¢PP’!.x\XZs$nR~p^WgtĜ ƥJ¸9k^: ep?ʉr/<| ~qĸ\c\q// \ .S^QJ U,7㉅lLt೬D jb.K+a p]FyB*ܙbv} U Yw@T^~e* ע봞|+*Xk=پOrԨcR^ 2TヘƸノef_fp\Ar\+f$@u 8Q,Fm\ރU?H Q5.A|oOk=",G*L25KyFj ZׯcmM?vuiRZQn!OUZE4֝!C(V-ShR ׭epR-(sr½W [4{Ee~OaܖTG#l-v{ޗ\U*T k%\/RRxLqq߂12'> |Nxs9YUb\/T0b -Gzw@4OOTL2lϼ RӠLsƹ#ɺYY1c+~8~ JヘT_xܿg W xsdcP6CiJ LEJcL,u²`pvϡ,WF鳩ﯟ>5`ywo/f)Irՙ.n5@0r'UܪtU<(ns/')w#WTr֥K_ |/\/?̮8ܹ\q/\K88q/ \Nx~OCW+YXjes]f&PǪr^~]xG UG9ޱ {ufq XT +/53 e˗s._289W̮.x8<%G1q(eˆ8 pRxpNR Ǜ A,VFk9pϞЕѭ΀mUDwP)NuRyN`zaˬkxIK6הkpj=A*u]tsM!Um[T2ݥT wԮ 1}N]9QE—SD/5_\5|&%Wq.87oD7_ 8.9x9x\} k?j/cs)|>3c1cII߯}p ^g %fb4ACO)}-r*Xuƣ~f4r ?$'z>qZuR:>Mc2+P[EiC2'=eq_D + . haƸ`, ΖRìNuF_(yV.Woїtpɥ|L-_hYYS9 eW:jQDCΛ9@1л/N/\3jsF41cb5x^>dQZqw7_*Ɯ43ϟDk W>#߮6<|DkWg'_N(]WEqrK8W1 J g|.88/|x_+%ps9㠿7_y-[oMu6moL(_y k[.*zf1Q j-Nh^Rq#)m.hXJӬs:>𒅰FNg9L+W꾥_xZ+Fdt*U`EIگ&#k`!ȝռ޷hBgȍsTcj;_ -i`seڂ؂Kt(ӕIBܮLf?f@&1$&Pz:y%Aj;h܇\&>G:?&ўՌ诮 @ԯanWt) i|2rSvdFM4dѣF4hF$H6r}(Rڭ9(0tl=Pp |T_K^yk8 JOW|k5)ө1maU^QTŇ}_5sHZ@ϐasSn.Nʛ[`9yxl Z4X5,P[=WF?cXԖ&~o_gr&%kk<\x׉ρǂ1"߅烜D|q;',/nXN1e(_N1 9.\#&q zYL4,d{Q-b ݔ0怹VM"oNy[}b+'U2s'~v{r=a9L ݢ4ts͗(: !϶Cd} %z__ ZJ9yph XmV>&!{ۋ\@ZRr\ r΁Ֆ(OS-7 ืσxb|p)˔LْZmq(x ǐ92 x\~ ޅ-XD:~^lK[}ۜM*"6q-VRR=M}LAA{~.u@2GZ^GxޗTB}kiDX(  - mUC)EtX@{=gfXNbj'_)9T4&=Mkm"eV/[U}-Ny0YZyIp))ym2ӽó&*+`36uTɽ]VMsgvQ<x2{>/Zc2f0y@3π2ЗgNG*ɈKr?U+ Qq$9ݛv ^$s^aK^F3fG%ˊ `\k_f]pω\.{ㆶ>Y!٬7|iKcݕx ϤD@.Zz>o?%>?yb[.G 4.׿6y Nu",4]~+}" fZއԉ;WB:YʏF==2G 9 Ou**S/Uy/)V_as=s7gyS>xEԆQVG s6z`드B߫C̗^CJU%H[oדy<.:/b16\UBl+˗e\ R˕s+suQ%D0ϗJK}DXSP-kqm C~}䩣0'O4( ?؛\AL/ rɱB寒#lN 4_ ʌ=2wymyh0P9%^0unοD ]=I>M*W9&"vWek\&|H5Q4@iuEoקKꟐuI)G'\Zz_* #u4պW>Xh ѺsԇX.\Ҵ<ΦQَ*T_/7d.J!'`(B9?$}C?i V,4*鎥5ΗMsSK kp) K %z(}G[G}5Kfy>|R|._#peicU%Vf.7k/"j2~~&Ԉz>\_OxB_O?BmHDN:+\MU@0^8zxyt1PCy:yUc{dBnr3/>w(@hNd(5/>Ht?y!~3I<Adi[x(ېRx(#8כDW=W]:b3XE>ݏRqjXt8S8{=bRQry;rLD fdf u_g^Q/4uG\4k>@EC\?sִIR&`bÈt4L$诰|5O,dӼQ@U|oV.*/xY+6yjrlg"dSvF@ζct1>G[Z/kf9z"mh`Inq`FKA? T.%y|toJR'ziM]IU#.ht_hFmc69Ifrz;% %ى  ٯYt+f9=; x*QG4߽m+6+끉|*0+WWiL! {ų{@>\55*/s.\.7ю̻Vq.aя<]'<gZ8\ܔ[^Ogm r[\#6ԩtQ@3k{g1PJ!}. t|^S@ ҴVU1X]rˡypD1q 3U><2#mwP8"]`xTIv-;Oa 7h>Rs@+ׂKgdE$E%N;_7 z}l{yh9SEDtWF5/?0 T!P,S1UJ)LWj=L|JOYO2X_SNl~(.hWF5c0y7-RgiyN[K%zm }kDFA_ /fr T|ƑTSeսlX,ImuzKlǙh\LQTؘ^J`\V18s7ϼ >?#CGs|G3冞Dkt:YRSj8!"%,j(Ё ] QRjlAB&68J~ ǟ*ymȹ74˲^DD~O293ȼ\5(BF;ƠE_|xdrQ+c9a9_\Ng \kJR7(ԧY+*h#U^*ˈ3t 7+RJrE]q)i*N69 Putħ.2ƒA\/˗16 㳍W4/<._ qkJ"I-yoq[w-yn;w[W-yo{Ԇ1\9җ%/O?O?N^R P4O?OS?O\?ҏ(&u䞬<OGBӹkފ*dIBD}'GQGQGQGQ?GGQGQ~&GQh&^vGQ~&D @  ?vN!|ph|uP=&?Cf21%=":< 7Ccͩy{/+}Pq/5\8Ut5ď9a4@A1 lGʗp/# KHQ[="ԄLѥpַNr֭^-3rB,׼h]j1G9+e[hι ]%/QOeuBSAi-\xrW~x)}J΂)qϑsjperUTXuvUνUW7KJo߯;^ ɯH}#l:^K3gߜoW_ j X0k%T~V\ڤsx( yc?o fN'AΤe=fxwZgsMoZ&aO<1NzQw%guorGlE'WbjPmw-u%̑nk93-o$9GE_?XCGmb%~_;%Fα/!*A2)y\y=jXGRe"_1^V!n58Z+RNƣY˘_zةz}^\aFj%@ ۨ r j[=/3=)Xv7 RզNpԝFVI}!mE;$e瀿 ¯(!pz^1[fn4,(KXX_<*YDv^zbڜ(T|HT2 >7kȋ\@zH-WO Wzu7ߠ@GGỹM~u#zKii2f: z"V/I_Ly=臿`F"O_<?Kxo_TQzUzޒJu} [B?FhSzf:ݶrmu^4(>'ʏ=cbUVq<`ݤ|;Nu}f%Էjy)o,*W! G\0`^ ~K3蒠 #!c?P_;W\uXHyV0w¢uzݚJŋ@1/O3 ;Ro9SpmEjwy\L"w?{L>iŠsss䟽LVRegAXgu玳tT4~tط:AEz4O@< xht 0/P<`xH8xa/÷@>? iHCWCqNA1 |xJC \,0u@6/aM@pT alp ?Aѫ%4E+r9F+uXGv EnN{iy] ;MH岇mrD`x!Rz{h#NiHϏ  ^ܹWx4=deZvӸ" YNh:3SF9Gk!)-3])^WYͬlH+Bsau< >e`V-@-vo< loQcMsk3z"uh}KHc=-'PJfaŝH J-%[hV c5 =/`rf/wn80LKCtq:ǘxB5Cխ֭Z@5NdANъeKJZ+sdKבC6Qqm-C5jEK=~{rzfhms jhgc*uG5r9@<Sݾ GzhuW紴NA6…TC];ԡ}BrEVQ)ՀN^\3u8h_O>l.^Y(p!y"ڱTw&yASyo!NY ꀾx4mKpvqa4o_m+:N$ XU)DpK;#Ҍm- Y >tqW_rQ`rNϑSȌլ%dL>ubP0-[֨~u-L1 QIXsQznŨ]XANP׮Ii.,@ eH}NiɞCu{}9sQ0.xY8"Ѣm{ e׻ݛKIOv_z.l-cIRz,d2H2.OǼmȖ]ҨΤi3pfo]&-ͦ45~ZҖ&@9EpjqЄSQk~U􀫐4|/va4* =8x4`QP(ŘoX@ee/CۿUP-Z_̻$T&{m7m4Xs0EG{ uġΈ73Nz+GmxJM6wM|fAiϜtqp>B; ϐ&KFffMOOˡ a~tÕ+*U~dpy;.CD5mjntb-OCbZ:FA _^Ir np ~D6k8XjzeH[&M9icL;Rtq 4!r"2y<`Cc*`x1dǛ0Ɓ*yf>Ly n(2էQHBs/Ӑw( 0٣{J^_UM-}_-ϐv/SUv Ee`t^y^`Ǚ]P4g~"в1ϔXf4~aM><7B݈tX~qy* Za`4(k ]F"G僀fRHtsO&F~룃`Kt>gy}xi(. e(e e:ɹtشK.s FJ ׉^vMV Dl߉*KЯ#Z!]w tB)A|~Cᯙj6p"L"/Vda#^ wή&Q -)>* ^ѺfiNEtF<\ W o_ayF0>Sr~p 'S^3~iUoj)=/il^:To:jrk&ơY ϝseԯwkd5kynGRt_"𱗏=Nl.6oĪȅL[gNpHΨR&?{'_ۍSY-G]-49 R2K ]V?o +/_K!Ju`5,Ј4]냦$_ּ@A:yTQ~o'@^z΁y]Unƒl}k©"2Պu?9Jޏ eysh_}`ȇ~%D@eq8Z\N))!OGh_V*'9v==D9P݇.W5ۀ`D'x3=s26uLbܭ$`4a=WN&0Tڞ LM~ ~JoTu=:] ݢu"抨BW8NJpzJ)7м㳺y~?{pk~ hF~oUlR~_z#Ks_76.%_Xk؀A~xr}_ysoAVy o0<(]yu2Ȓ f4 s0vO_i1:]#%\>aFiAG/Wwf];}pƘFVqkuA._.NUk|߁V^'mr,Y~7pIeo|N"7]z̫8]yi؂U |=M~k3̨j?x_6:u!M?x2Vnd-b ]~Uי4MusW0:GW!S-D|壩iz(hG>P[z>$퇳:c[5>O]W^yob^ӗv.#թ܆r0uej4 y|R/7L {2r.ElOH^y忔æ:xC)9zSM0aZ^?s .FN?# f`Y&3, _Ke=c9T\uŪt Q3:B. X?prS}x}8BB\YC寤NI-| f|eo*̶HMz@5ևg3廂7*cB:]zJss=LDc7sՂ^h|AQej]:y\W[^s06Jk Btm^*$ ((:l?|GvYzEnjhx)DZN6џf#m0:-"j=HEGߓX͞@7V}k=**iPE/1Jѭy`nrko-/ PS8C G)=uPt8Ҕ:wR(k |K26op*[Xz/9Qy-l|~BډӇ!ZP1g=Kx:ot2Zݏ}7+Z5LcNF5U=(y?Yhe{3AN6ѧ@%rΤ;-u@/8 a{~.#{۾8+.BC*=*o5+r4qQ;֥k.~_S㓓 ۡĹ־'>^vv\?hWW91:=8qy~ i=dlp :(z&(uH+k":64A(=gA6gM Ps9k43{ TbJmS_+0!HH`N-#L+.n Mc`W+^\([GzL|A^029" K^upbsrp-^"@fcn{70&_UX/V1gXs;OG-11#MQ#߯f<gEC>G3<2O˧HP4۫,bT%B!#<(iFZT[aX 5a@ZYPfǨ>0fW2+x3ƸT/a|13*W| b|Ը^n[DÖ9ʊa ʖU1zĽ@u|̶ViE?7T}6Q>e d\Pk6jEpW1Wzv(b_ybfqJwWuw0 aEJ/5'¡AIS4ӀbU2g5t+ J fT*_J\+p1 p1 Tľ5 x+\LJfk*W pk>;"@kpfĤ9*\/,Bj "GH UFE ?0tƤHh05\Ev&L!UaJ.TJp \+\*8\3|+fW 2"+hR8 Jk<朊003"BM .i!0E7*4Mw{n:iy>eH^ez 5 e\cu;g#2@V܋ Rֈ9ZuVz2v '\ o]&r+-i֦ιP3(|n%@) Z} 8#Tj`4}B+8?^ZyٝMv) YI zjsϜ\y 6MFuL7,@n =H\t,Xۤ} l.أB]E]_q = s7\ҶA\ǙXn^N?۱ c_ *ʕ+\*TRU&x8P*¥x*Tgc\k1/ %EFZ%MA :bg=sb0JB5QbuΈ1FR-"Mb,>}gG!jF9.|Wՠʪ w^z thA@q\y'&0j/"¯# (7r]HeJgURJ %ppƼ&81̩RaR x+K^<*W|Z`8Q qφxR@p¸fRJƸԿp*g_W|fxWׂTJ8(g C:9 l C(AQC5< %w.)$-];G -@۔ƷD\@5X]ziV)|ߵl{6Z{"i(7Fuxz^qWeˎc>7s*T1Ƹ+RpRJ+qxW.^ +p C0lj^<+R#2pRx+PG< EQTrcˋ\ Tj5= ElȢ]0m%C\2 pPIU#_Pz[mKC5vq-'x,\IoDuJΐŇTlj ?`0=G2X|zHg+t ] "htGdlODeʯz!t+\jWY^ ׈x++\k cp!RF|f%x ʕW{B#KnGYlP)AN &<+_\kC+|kKrR¸T1P^&44پ]&T-;?0>RuPXϓe`[9rޑ(,D&f1çQKl$1x^k& BmAY~BSa/sW65CSf*^R5sPJ|cXz09 >ׯH50_[kd;.|Z]*| _6ޘrJƃY:^2fʫM9i^NZ$eTt0a 9Nd#Iܬ¸T.<|T5~ bT1 ~:W 8W \B5*W |*W\\Lp1F!5U//ˋQj.s+-yy^yxkCv~ }??X(.mj"Ȗ ~Ґ_SSNq˯XuAC]&рOL/>Ƭ 9Y_7K2msȻLSi u:s]!V_GUAxT4yF0?O _l 8~,@ >lD`;xR3A=f . Ecc#X Yg۴̮*W\+p\*&0Ĺ^ 牏 oį|*1! :R5 (TA"ԁq}J‫+g8^VcK>CĦX WL}?n^&XaW;`(J{ ˮɄL#MD䟸i ib]}~%4e-g*C)\Y7:}B o<+@.F0ƴauzqo 湫 M\C AO__+prqp7ƥp*_x 1¡xp1 P׈cLcx\7+-p-x |*{t Td!1 YQyC1f%'9/oW|z:\|ļ5.Ysw-r-VJlWrWPalEau[u8/zz:zK<-Q&[-3rkyHu~Yr4k!kO.G; ІӝJA^O69yw69|ς4պ]4Ӫ0p0p2D#'#VX=Qy]rAAzPw*?O>ҽ2\[Qj  V'OJRSZ[Ӆq4=!7¥q" T=}t%p+]?L;[txyO>ʕ J+p3 3NJǂ < d@RHU^F4{+2_^~J kK5T%:PЅk_73PBLMZs+[y)SYSZC,XxP* @!W v n4F/7h,o9!96%o>ҏQZx- QW͕ J_]ȋ'A9XN S>!f\0nQ,(iv5¼E9mD&tyCee~б7.ҟcCJݔZ34te6GJ5" ]t-A~%pX@`c*=iZޟd o/5PB :/.w3̌:AR ].[m?<%bJKh/s਩urkha#h/CkOް4| O8c\aߞ+Pשpg}eC1YdB#ߟHiMp <O/2?HX b0xtK*(-4F]ݶ1D͈ ʂuOljy54nKKOT3LxW._qumC O6Lߖ%g>d9d]z=a:\.)]e @4 JXwR凼e+"W9}&aoW,'Vd }`-xrʷ)_+hl]xGTC_di/B^JV2qrvcl ד P:=aga0J~S𲟅,\]%MtٍF`!bxħ% 妲Ag:~mC}XMSJiO)??Ulm_ZY0yJX<:cmS8vnOi~Lt!nEGk+J'F}v9 zvڛs;B(u#D%u&ShTWNJhs 7 N¥Co~ |eӯW/ ]qZ%+|`˔Z ~t~#y_]OY5> h,)*Д9&E6FTui_[_!/1JMrrՉ؉䧔o:FUH:Q Vy,M^01nkn)I) T-x1 L?1DƤPơIk+Ueʠg+q)[Xt\߮RTv2se Qhʏ9s?Ѫ ](Wys*_/i3ώ1 ]0gGLW-QCTTR7:X)K9G.)#?eE C > g$?Z8 G :?GoI R|*v"ޑ:TLMRA_iBRQwq h(+ì׆/jXP~cJ^XpԚT+oQUp30 ZC3R580]"W J%^t8\n\CmC4 ׂP0(-CtR[eq^ G8:N+|oRWkG}^*%TƼ7.Tq"|/<^/`ɢ. X)[///-/RW aDTBWLB,hGZ*X)WЈl\+%&u~iw_ ~0J4M8W51B2L&4ˤ^6 g JkR">jCpeW]b!ylW,b iP*wJsTH Ұ@#[}+^Ph(A(t!V> h}ڨ~Wbj mkB4 _W1Y]y@3ף2S-GZQjCkn眮@< 5'nw]tqmo~7ɋu\ZPPOL_A39YTzrVzKSXVeKZ4 [,2 1zQ ̧+aՂ)3 <#H O0+xX<$8$Mu.פ-ClD^UjcJ6 `7d6Qݺ1FaW$7 WB_YLl8RrjO%8ݪ^cglZh>Sy &uUBj&KֳMڥei{M">F~!BqE"ZJwv-E%X+*{+6ou@ so:lAL}n|)6y:޼ɠ`xt92uZz fGGȭ:E+g$ua;e/ ʳWKflbyNJuozީ&PzL^u;YyQW4 9,V|%l uxC@8nNkVtjvfPJKBE^wP|C^ I86O%aۢ`߲r@n =j'}=JGԁ+@qF~U^g4 0H4h>r\зP;| G씷SbtPUZ\>6 =d[FkLUBp@gRD $2h2>VVx@/Ƨ.qaY z&}KշG$<@Aӏ^ <QNx_1 x cVL.Yu}`ê?AbwXz@ń@2>s / a>x' 0+*W=㊁%#V|/HÎl\ ;ߘxHm>Q}"¸U%Ο oq@k 7pcmɫ@}DÌ0QEU7qg$G+u>!)_0~*[l]T2ªNCͣ3QƮ|AA-l߯*kS?1_yȽݥ@ == **܊sMbhde/8 5M*dO` (QEl'UKzYnI}Ds6p)w3*SAAt S9PWH λkǙFIѧ SfZ3롟<٩g y^LDTBVe<b>r|htKx $pwfWU(خK]ՕplmYN?@QEtx20j!r/ۡ2Pъ)5&#17ul}8B GRj~&x3 <./f6ŐTa R"z=O(TP]] >}oh ;Ik+4591`(?=th"z:W;y:ޫ2bNvN`@PQE^k'p.>&Ch'f^s܂hB'?zCGS&#^];3_^}X1>CiߧMfWO3^Y&?abԤa9{hL:E4<Uecsd,MRHesAF6jB^~ 744Y:rx[˧0Ȃӗ(2E@x 8Pw~ B̗2:#)=Ut>1lyw>'Pp@Ś+Mفy^!5 `^tҒCfvFrqa!ARguUg^gἑ<ȌTB 4FT -uf6z(zP>\.ᡙ _W)D/*<=N^}!C(s5YQP88u~r/={OL|'x aܚ3}\F;TkۑCĻ5ɀ;!$h鳟h.,CWޟ1{}"4 xVp5rY~NorhȒ %]Z[9|r*u#BVdb4~2Cr!L,ʈt @`T )LC9e[\G\>Mdk,(CR漿24KV91p^KNMl.V z1ab&]r qB^_={t& D![o.,w. rETyb/8PK+hQZ4+^+3asA|Ccϼ-ymm[n:nwӼ6 xm;iNwwӼ6 xlg>ϼ6} xlg^׼6 y凿Ex4dEC5)i D&f<X:ĎFMޯġ.O,n8n|ıPĭ"OemA54]ãC]V= .C s)jhyDvqv>'pD%ͭ+7RgߡǗ7Єyq6Rz^yf|?yKpaZ3<[GOn! wW,0zMQpSmMT\0:>*VX^2afvfvZJ"5O?_ܚP g&Z֡M-t<qZMP񆰇<q<_v֍neЯźNG^n@jr0%$qs/Q!!*ДP&b!1V]=e~C/n"kX8Fx= ޗ U7 `tS+:!O#/9'Uw&-$IE_(8)5]ub-^ֹhDؠWRzrcM!V /^O+i~ LxUu1-ڏW DrKc]Tx~^F~WoK %h'gS]ToF;'2V\͓8[XQP<`D V_gܣ3Q.W E=ax1:M6| AW/xWu0-}HB]y# 9@\s1W?W /pyP?1+]Ӗbޞhe\ V.hy r,|N0.$w2h;-+};D:yg/\P%Oh|lVV3`Qɬy5t/n{ЮMy 8WwB80G,q\E!dzx*NZ)r:|5'ۇ9E%^ 4zJQrSy|z]zM "ät+oG/=䟽a-4iopƹt|yM8W*rF"?ޑzdjύK$'l#4p Q2*8Jn t;κZ><:,%yQ5rgh2Q}j Vjn;@h@xXbfX\O:a9z05 ak@&`}St۷GZu@aLgO<:]xZ5[5 ;jO!?Mf @>wor%<TW!oG+^^;@ܿށ5TE>f ]DuX>_mf[m$׷Y'B#%j'4MXܕ]awhՁ-;pm ד P:=es*/6Kƚ Iu LoCؖ39cjo0'~> Ff(m`򪆨]N槌xjNSY9j83=tO{`[a$vc@]0#Aˮ`2C8~ëh1\EY%oR2n~u?}gc/ο}gB:P^̴E%Zb[&K \1`/Btr1 &A=m>" n<!~<Ԣn^ k׃,U3, zDS[J -r7K+ycU{Ac₩a\ᗝse Qhʏ>Pdt2 9B}[L麥 V>8^R՘K_f 弽J:=c+{&xςPSr"0\QV^_˚ .:3S癋!)Sp^])3\&KW*نa~=5jG;G'&r/`TOG?a, b5H ƥ}DY9GH89G>?_UR8Tb:Ĺ"p2寉)pZ^{Eq>!I^-}V~wt1}`cx. X#I|+3-jRWr_uB3 LOk2-9<*,ϠD+y{Ě7J 5x_xXR8& 6<⊫YR۲@+sބC9Ihp׭t #M‹b- :]&uT>h\iL=.oĹq~ëh-%xptxoq<7FW8K DS6Z[¥xn\ 98]\*ZbAew*TR39΍oy|ʕ*TRJ*TRJ*TR5*TRJ*W RpW ¥xTRW RWJ/ʇ Xn:tHD0hѣLtȑ#D2?  8`$H"KD9cI -2tѣDf͛6lѣF,X4b 0hѣ4dՓ&Le6ëf̛mfMi6mIl&Mi6mIl&Me6-[(QmEm-[(mAam ~MOi6/i6/i6/i6oi6oi6oi6oi6oi6oi6oi6i6i6i6Oi6Oi6Oi|0K]lM`W抢<(I1*oB.Qt(RpW9Է91W#c,DʛSse0*.&4MCBP 2mmbTwL׀6# *H]SNacJWR^+ p3*TR*Wό*̯q ^3< +Kgcx x ʕĪ+aآ_5.4%50prrB:XBk,l2zt/q0cb_,3-F"Zc7DlB_2.\Y.٨yT͡q[+.]mjWF8ɘ@3@K:0fPRa94~?\R!\ *K0\kp1.<¡8^ ¸ơxk^xeJN xՃpb=Y\C n Ԙ7'?R%ZTMDDC( 9EvB4:;g{ضcї%xkYQZ7Pc9irX敆'#h8=,%tuVb'@ƧX5ΟuMba?9)ԕ%sG5#'Wy F25hw<=;5*R$c&+\D3\lѪ 9deJb+^:fT^ RJT7 ¸R^c>;gR&ep q3+b* #\ @ViK.iȪ?,S2(/KTqPb ZrD?d,f;RzNF0V]Q`^-\a z穚 Vk]!CL6mT1rש VVPSճZI_Ke6+5uUzTFq" BerH8 Yǜlj=RYͬ GAA0Ύmjxe7JK 2ƛԩНB^1ifŢB7];T zNC#]UÃWY+S5n:]V ƾe܅M \H0YJW9]*B{6t*.m"Xuy8tj#YRˊu/PJcu&(S&''+|+*WpRJWc@fW R&aq?ľ&|&#,7C%ҩ>3yO޵-h Y}2U/$9pNfnwcoZ@nPbeBWα*lM+dd9ev*tz߿Xo]gߡG 2ķzr렱OpNa;=sUr<{2snyk6t{X)ۘՊ!?{'T^*T^bW W \kcp3ʕ+f/pgT3¥q3ƸTc\/p3+b8kbaalbAIcL 'R|t9 0L8 _6 `јO4CZUPlwr7]=*XYRV.h6M=^sf"UH:{%zbN#j&]ya`r] 9Ѫ4TVmxNձQxny-30˗#zWUjp!#XHWFd^x5:GFR7XJ-CVXxIipYpb"#zke0,S q£+㉟ J׊J~ \+q11ƥJ+q~ \*T1p1p1 qp^ 3pQp@%ur<6PD1j6yLK\S]`H[»gv +^ Z6q)o (".( Al~^~U'Z}d/uv Nc.98x߁̩PJ¥J*WƸ^cWx.T¼5 %x JWx ʕ¼eJA1GR x.-p-SִGx5c"w.h p49 @]&pRTRtĩǗC.PA.$7r͢:%gH*8d7.:22MgfZWS=w"aS( uBiZf_p Gn{1 ¼ƥq xqg\1¿¼5Ƹ 8 +gbW̩^5p3xjW̮&<"J\*p1r-EjD m5/c91rlěXOx,hNTB>"i^rP:1pEJ@GsVM`T)ҧ?圕ř &w yץ^|9[6&kW%^AθšmCOcjɇžxFc(/q p\jT Ԯ&%qTq 3P̩\\ ƸT3!~C*WxLp\j qx*_2ž )@5|9<(BqRPj."k BJm!@dG"^Xn^!R}|QL **;ͽltyoRV\˰_')=nmקp~* *Wrʆ%J* q1ƥp1̯1 xJ_\oRW2Ce*W 3+\ZQEQV,]Su3{/098z ܭJ0r9Bt\_vp%k>Ҽ$yy'7H$|Κs:8,UUcz%98{ܒ?*Ooי|H*C qׯO,D_!Pޖy#CK>\h5`w:Z/;C3袚 4nrRǂxkkpcxp|.T3*W2W C5KM%PS r9W) CmGߜJ?fKѝ)>ګ:iz%0vJz@ Ba-}xk\LR¸W Z Wg3 Kx߆J/^ qPM>bs73s71ćT.eY=l*hTFD_HVꞰ aԎ'S>z֝UNyaoLP3,@޺}0 ņRi ˧XZ>-ʅ!ˈMuӒ :2-[\9 ?,$ڇ[W\3g: ZD 1[W%mnu~Of'i׽ "-Vw[";EO.\*T9zÊۂj6I ֯)KZ ۂMo3uC_ kEj"4˕9qp%~` `W+~/r&n|E૕Ԗ9@DL2D8R4#@j W%xk&y K.\SO͌`Q*݉TЬzc~IcRp-IR5#H)cΦD;L)ض\_2+ &<9H56D5aZXZ-y#ɯD Q+;z*u½&۫nm/GT k0kAtvyOEF1_1}r+9mb9q16?0P=V(.h.UF0,GbTHzḂGyMT>kr\P]/5ˋʾH@Mocvr˺Bh~k|μv~󆚐[]yT&x ;`lǮ*6*`. )-e;&O DT'Z7v&ft +á:*K%eAC< p1Džr_>lb yX%)Jw%Bҏ8Z(5LK֫b0=ߴNZM:ֿb].B\ L:=e4Tz/'u=GuoGۿ(q inGq `7CxW&۽>b}eԷ[ozpu:}Ne&,ZqPD(i)sf~Yz:L3C9Mx\<_/|ݙ/-/W29ik_˂݇\y`>3W ZA 4^] T+KoKE?  JCa-\տ]kht=۽gAKSHf*RmJA#uӁvW q/9fvRh> {{h+sK=D7t ֱ@eT]%eaꎠ4eVys68ۖa 6Ś({QʮdA^mח6dO}C+p{Ey t9iri_)(Y ɾHvT'UpQ8<  ]G؈@?{[D}? +yl #Y-O?h%x]xӁp"Qk^ۆ| |oRW<$8t0icxo\P J%"9B-J;bYtVT΂`EÔ؄ >3r+ƥpԚ%kW_YeuX(g,r@L ϚD=[o<n^PFQRNR"UO}Ox|;ʑ9H{3EPu2 i¸s,BJ#LU m+%K&4T(ᷧ3=7eGQn;Ԩ:V7 U[r: ^$W!XĻ%*",ˆ8^ܹ|*fgLj4yx\*TRJ)\3JN0_6"jp^ *W`Ջ؎7)O[凜nR4=˥Lru"*TeJ*TPyB.6X1UWpݖ$\5+lτFSe+RD\jWƸW [+R+poiiiiii6/if^l΅ -FE_fKiiiW䗴:9GzM[S}n=nn=n=o=o=n=n=o=o=n=n=@6Ⱥi{M{M{K4ú*Ⱥi{O3{M{M{M{M{MǴ{M{M״{M{M{M{M{MǴ{M{M״{MǴ{O;{MǴ{O3{MǴ{M{M{O3{MǴ{M{M{MǴ{M״{M{M״{M{M{M{M{M{M״{M{M{M{M״{M״{M{MǴ;MCheV*T&hnx_G&[sl. ЈSP69B k04-򗫩^m RkЂ ( (iP4*=߈".H%DkcX y'9@{{DoA~8/AA2^~ W/SJ_+ _)~%?ħSJO)%?ħSJ_+%įSJO)%ħ~_'OS?\'_?kOħħ~SJ_O)~%?ħ?KST^+5' <<<<rO$O,O$O,,/ygygygy!nR[O tQH<Gč`x%H $ q(p gPd>0@,cЀ/gAjn PN!.h<PD&a`G=?1/?H&0!.p>Wˌd 3>8L/$cH 8cW<‚/@`04"ܼs8G8CSc? W+obOE >XCo )o|5@b)9"akՑ瓗3&t@.=jy dȂiHcr(YkY;׵.!og',9 ~@ry"cu<cLy(J[ ƖXA3xϣucO- W#_׈Rl(<[Y ',yǒLo6Yqprb}t|̀hBB8]u!J0ֈSIAzNkkzi V%|.i=`S(t4bW!ߡ+ TaL +|AݘU~M_^ ڥadJ_-oU5qԪ.ZBt,j2tWшЭ/atɀ齋 C )ri J;<\CJoG sD3ڬ5>z1lUQ@j!_j_KzF7֬TO" fF 1Yy&b]Bz[``-/.Tì7@kL+(J _V¾b U_SџRhhx[+˅ Cf\<զխJ*oWp/4]Y֧5|dTS}_`)^7%_6:Y&ltȧΕt.h4>꩑r C|̃ tV͒S/½b0+:WG*f B(yTk96UhhN;{q߁bxA< c|1F(=a’sHJ至1Z*]==eoH1J (,X5>ze0UʲG#XYa 4}O\yM5"P4p,gU\D")fhp,twq8<َ"K#vYqKFB0J{UĄ[W.~Eu%<#|*ɡ0u^sc-%-::UwLW4Q-sqE/QtF15W-֨a%<\^ ~MlC !H_&kWIewKC-B',"PsCَkŶxL^ 'ȆUWUpcDźPx  >(;^J\ *iT4PK,-AL:t!P? QqΨ|8ȝѣ8%!?@`EYWf@ASQ#2ȓ0W`1.pa~ ?3 M{8J>?3+䛖 (QEl'UKz⧘p\{ 4>XE%? .HPq`CPG ŋ91uU1 l /_P (uJz:OhcQS' =j5_.w;3XGXWd9zO c 9*^oi}7H.@PQBP'`5I"j/{ԯCU֨hr~uzێ)Jjm`UjbR8X*ڎ\P(e֣Ӽ0vF7D(7Y=q7GV湟WX 0ydb2CΝǨg_ktw9sfjg+NR )*#[C+i{/Z8>/a٤2 4'N!鮕ʆ05L][7~ɦ=:<.f;O]_,3@rff?')@t148PYO!fAO1}Nq4OQEB<Hy>Azߔ+PYB[|򮐒Xx~:tw:Md&y4ݟ;^]<? #Z#{F'$W`1'zo鯜x=^\0R!U <g+ڵjL%uRuϟXZh-"4@m~o?X72}x`zph}fdFnzEԿ5vrsM̈́|i bX3][m@PBWĶ9}ヤpH"kR.G%EMns-[%4>D=,m_K +ϭ) nN%W}b "湿(fNB:Ds+q-KK:=k \dl׆>_rR[sz]cypZ\p5}1A4ttGT 453?3Cdbwh:ㅄK/Bo; v*r,sywn^_fZi+eF]Noj]lל$Elf [旅SEݶ5$~@:FXwפn.e}X02޾Gϓ>җfѴ 9օO3 +`ͦ<ϡcȕPYWN!sݎGf"q-CU+'26 /}Xvܓ pB.M7lΧ[9W4_.Q'~v4deqT\K#K>!8{{hk(((J#QyvJsU>t=_aݙzzC DFk;?oe\Lx&DsN ߇0|$%i0B:ʕ㯉d5S܂:pKVӀ~op 0EAZ|U~IO&–f|?EI8$z:}F5uLk|J#^h(/X5bMS[|Xг}Iq1^w^;F^xwa [*"(=g:4vNvJ[tZP#֗ɰב?+=zƞBR=`6p?D[׍Dc9Ki,ω~GLf~L`:֮6O;Q}=k\t4-l8{_C u}%³Q(j@Ih0l>c+ f2az yOH{7JĶz^7;;BtqXqX$ÊB¹ g]qP.tC>_܏W  Ŧ z G_˧-uoOF2ǜ{~0u@ phx-5DtuDeO~)WP=ke93Yp+_!ޢ?E[>W 8H/8UzuQm7 BYۙjs][Zgk|נ^c3-Nyp{e}/QXl9궶3!zzkW`_]f46圖ƯӀuջa4> 4NM jo?5fj. hkT آ^|2q--E+x:O>nψgmw$Rjj>'m?sݿ\=0xG>N/Gϔ{c:'aϯ]2ђ\UzP<-iAN5bg{0£(YA9GІlyLye}hUOX9ʙ+:ăD}!mf:0<McDs\9|)zAӍ- VԖ5܋C÷x σxNNYev `(ԭP8-WW}7@ kA*n_

yD]ā^' ӯ Ƽ)R dtT5rI&D84ĩRJRJE! *^&F yBh !9$ *f[T̩R"Bk^*WKx-s\c%u3 ojS@vJnEԩS< c{Q GEqWy: R.Do~x}GC_j¸< q\8*W KK@+4v:lYckqU*:HYMPL%x*%Wx)K+sP@ x OҀ8Ffn3yٛvf?3 7fo;3yٛvf?fo;3yٟbmRVkӗ[3 vgfn;3uٛvf7f=Y߱Zŷݪ%j@MtAvhZ&;g3ff[t]=Wfua>c?3}ٛvf7ݙfo;3qٛvf7ݙfo3}ٛvf7ݙfn3MyUivf7ݙۀק.rm~34 Q֎76n3wٛvf7]fn3wٛf7}fn3wٛf7fo{3ٛf7fn{3{ٛf7fo{3{ٛf7fo{3{ٛf7fn3wٛf7}fn3sٛff7}fo3sٛvf7}fo3يQ D-wű5\ /* $Ĩe XG1F"J@U\抋Rh\*nNξj.nȚ5Y @4 cL 1Rެ##3(k^؏x(}!vqM9iy*E]K\BƿʥxjT\1̩RJJfT^>0fW2+x3ƸT/a|13*W| b|Ը^n[DÖ9ʊa ʖU1zĽ@u|̶ViD B7|ȹsDcYffW S6nXu\iDK&a,¡AIS4Ӎ pWJp3 xW / x¥qĸfW xWcx*W b_<&%xe35+85WV fpC y 5l3RbR ܜZKVYOX 98sfQXUMkT6y/,Bj &RWa:z|ME4ApςVcҒ<0NK4EAAfo7a:=qR ϕBTy;4kCb`u&*,7J3K#m)xNnXÑf@~)xuXos]) ;qakڗ=W۬ S/7Ҏ\+gƯx끘cRx+K+R3¸W +J%xk J¼F|5̯*+%p-ⒺaZa,"L!L Ƞ-SENM2U5onE泡+a͢ΫLy-҅zAZ k*" 4JY.KXt/Uyf+b91}0<:*]= lk6OV{2E:_,S0 'raAV48l`T\ =;)X\sW斪skT-i{hywMGyPvyΞӖW9qqZkz9aב|9KX s&2ܺ}qWxoJ5s:[Qа|@q95t)x>ChX50͹V\yht1EAN|,DV)WV)ϕR+c J n\e.!;MK2ђվA-~CGuQ9Xgin6tJa`eN?\\¥J+^ g5\*WKx<Ƹ#_Q*ሡ^Qe_(Ma-೪6)ks6-\.)Dqn+Y@)5?VPDoWfbC؏=[ƝyM?}5]w3S 4g00MPgUehM:iOQn[q63'e\FnMby=szi֎3<+\k\*WxLq\3~ce6F䍔 ! psSXpA ׂ\2נ}*/3woO?~>ǝw1P"yeb..98x߁̩PJ¥J*WƸ^cWx.T¼5 %x JWx ʕ¼eJA1GR x.-p-SִGx5c"w.h p49 @]&pM]*):`]% DbMc}%GAl]eLeb27{E>40z0{^Z4]CM̺0r"Oa4WDirx+J +^PcxkqT3 q*WόįRjgԯ\LxEQ_JT b Z.&j.%@_?(R sbى6 0)R偅6V=mC=GF9 vJ@EU ʘ賟%eDxcfB扝w>^FW8VM9=emi!dMi|N0*^&|sKwn;Ju!/,vb@4=BUn)Rϫ5¡+JWĮ*n!&|*+Wk |3/cbTcJ +C0/Kf_\S1u'P.7J CCPi#N a>6ocs% % EbP+˳5:Φ,߆\'7ߒs皈Hoތ֤I#9Ei#tә1bB4g.x9/Q)SؾV{~z@@Ukzyb%lmZO:#Y2͆Q\Đ?iAdyQyМr5t α= a?L6RX™',or*[^*&<+_\kC+|kKrR¸T1P^&fFXHv9 v{%,w#-k'TR R+\jWqe+|nTRcjW  kRR>/ p >+q1|QEs/_Q'z ˙[eF3lTTp@x髺LYS?Psw.)$H:qKW : DP96q^(SMO,Y͊Z o--T,C܎UmC~4XU^3uwPȤifgX;z+rr2zi)K &Q2DyQƀ9mJЄVs*6yَj| z_=R4J,//U. `V լ]qR&g - ZǙRN78L/xꁔq֓e<;2ٗmyM|yjx]c+y#Rth9m+7TgBf$= 7އ_q@yh|2鵎|Igkzqth;  W+UNHuqjCgWxk\LR¸W Z Wg3 Kx߆J.t6`F YhftVQ[5Vq ?c%')a 2VB9ν`ʂ Ws ϯ8%"^Uуj]ozgX/U}2Kܵ=Vjr"o ȌD*U1vr^ԬA /+,7F qa+y6bOpye4%MK<ۗ]9")1kR5].^q`\ͪS[@iMyZF?~@UMHa1Ɖ[$@4|H0G#&CGxEִ\fn{qbbu9U~ LU*+,nH P0ÂD:$&D&0`уF`FMm6milMa6mIm&:I6mMe6mimzhD$[6Ql Aim?M~ͧ~ͷ~͗~͗~æ7oy7oy7~͗~͇~͇~͇~à7Oy7Ox'~͇~͗~\.nJvN B\t, zs-~{LUV>Lmzfk) J #wKboX n"z0XPR^h+{.5ui^\?D:ma dwlAZ[h+y^ͱν/'QAOO.Q\[^1|DV2D+<{Bħ6(1ͺUzLc֊aBOmO4Qd,1 l~pj敚+0KCl1mo#˝@nd5X;ӧ*|B(iOj"ZcT|,' 1x138s z_tF?WtXb[2Ti32`鹇*tɳ ^<65O2Nk]96X܇M)vCPXsy,۫wYKNUVl,$ Y>.3XLAg+"Ar3Y)Aڵc-gs;֐:"?Ktwߞ[gh)QKEXpnua#i7IJK{ƫzZ_9izhzj9TҿG_~maf8rp/C0_2d⠲0ZGNr&8Wjfj>$hD]RUFMEOR J׃I_8Y0|5)oST#x~ 98bFj4cD^YE5"iCOY֥(\ #H!իUNѿH<#8ϧJMkoTߛ59yWɶI%mMQ'Lyńrk]m!‰dj ´Z/s1 G_d:Vw-=T {A~MV/xcp)' aR:-^G+D8*jUGf|Ϝk(ȸLe)Mb^:41U/2q@mGq.G|U0MC^CjZHU*#)d353BڠDϚ3W^ɓDH˞e(hP2GYL21. " X,x},P ?7G&=_h~R/NX}O/^`2Ts!śoHf*}:קRL$6C_:&BѯaE@tȧ>2uq Ái?r̰Vf\[I\*ep7GYzZ!}з#ƥxKN> q{}`SQ\Ktϟ1̩_O OX=y}cE}^_~e@!^~LQZbgس!>λDE/Ϝ6S\ ykKLtu}!4Tz.\=39호;'z>Fˏm6KKt=mv?H{kѾ* k%Hse@:-KZyc\t ~^S~GĦU51iMqX\'pxB^=sV>= #{7)is^a鱿W:nx+eK}tq_fҥ'^^Ih@G.O~=74,G~K92 hCȄLOJu4C^^Q;ܙo]D+y@t5=m!S#KtS~]XЕl^NsUV0|auۖa6kG*2sOK}y{JfDh'>etr(7^-jp6%aHDLK /|+ tzMvF=_aS=YQ-o359,ZyזKzY^HvC. vou1Gm)HlTؾuqh̏9bmea\/lgM^9˴ =:tIV|3.P7;Oj \ fy0|?Ax.,\Rgrw(; D| \5#9GW!QϗF< \A5,5b_H-eF)+b=W u'|sP0_*fuƦϙgo0/>o' YQ U*TO|DU0y#ŎT//WțpG#*QAd`9a4 Ɉh՞"7 1?yLѫh5I#Tp7֒: y[Gt/*1waCx %ةQr>g]4F.Pws=z?Y[_>s^^81ɁZea;:|Dzc Vm-+K(y +Yk%k{T_6q%.g%O:IēJI}{ PqIwStֹ}`VkzzE8M2a+FbRME.Z䮆̸d9ܚtggT>'Ry}UAtU29̹P?j]G2pjeɛHwŋRدhp:y9G%!#Ϥ[e#(Q@ar2N2dwr"Mܳ㣎Q)'9z/XܴV>Mrv<,'}K>JZtmS=G\?1/%\fT. *"N؝Eﯼ[)ݥf/%Ӆ\(?1Lm|E\9SWJ`re*:jvq(ھ*{_ߘ7 fi>m`iVWykYmz{DL@kÄ8r\fxJ(^niVC Z)Aiztcdms6F kKE{7Ч>[nk>լ9|WZG-TO./z)TȒGtmɷWwױ1*T!kߴ0t+x5 c,Pr<ÒV[s.W Yf* vu+ғMSF5iu-( ^Pd*>^ 22l|xW^xa*TR0+`pxi(<eKx*T3zsxԶ O,]q47+gR)m*W2J*T Rђ13+ݲ[-xT-L-4kR2޼RRWYu+RW ^:>fe2QvAG8ȟ?ӆN:4"ǎ][ÃV|N /nX,fk?1/?H&0!.p(+XҮr5u+[T\n([mtpA бi~Z6݁[\zi[My 5wCZWhtgtFQeaw~FeؠcUz fo$u7 yކ&黾~CZZj՜WWieVf*ƶ'RVrstVQ-V&KP^/LʮDU"qaKs7NCJӒ~Xlԧ:6*V9b/6LI_65,wR4`^wG`i xBx@ #엷K澍)͵<AyU`A^T+N'*[1];{q փ' m{c( [>`Un´XЎ4:ԩg ]lu+wa6tbyd=zk{!K Xm.`%tخY4.P6W+T }ײ}%#]ZϼNP욲`[fѿ^viк<yzή4ʷ*2(hy:yNՏDiY^EѹaPz0_Mٟb(o\7xQ-zDmn򜔀dC+71'"ZEcʄ~m_)kzIugL"g7 7꺿eFhnN5`..zz E5/9^5:MXP?<6z  띧59Zܐs`>WHW5xYcWG،(vDpHg ÝrsA>uu[nIf%9taCzzZ EB݈rZ^DUylY;N+k^s^ wzr^ψ W#_׈Rl)ѐCijP'H@X&d9>߲ *N. i:`;C~O"ӏRIUCO"$7⽍zcDqFuu6>TdmKhǥTo?gؘfc 'BF7R%>%8>:1Z)1VQ,t6hpzc:n+cs/P] G$<@Aӏ^ <QNx_1 x cSm:{ĶUUR  x,"`qxS a98?^>P7>@|Pv蕎T {ߵ`uC6D<$6(\q0zPȢCo=X.wEH@&ԍ@n:` ,n3Y~ ppv4x,ޖNy|evy|"{涼Kr 4~F'q4{/'%m$<<40jTAr1PIK*c :,kѯ` (QEl'UKzvȁ9tۂpȆI S>q:> Tll׫3B"Gh,XgTjdu E|g=RQkS^Z36[ 9#Fmy:_ 'L\k(apkƒCjTFb,Vս8Rj`Qp=b'~1 5؃/1~r I@$Dž\˩*gC9'(8o~}7p&G* *ӤF3Vj_áwVjb؂'TP˛D94*?8gZrXiF:;/]3^1X` Yǫa}:2WKsM`P5psͮ)߆~?]MrQݚ`rwި Ϥ( }Z pas6(mw?QEe q!4|8 ђ}H4KD%-%>]hFd՞IsjRmQ b(TrƯX:s '+m=<ܿ#K6DwSg6}}#=F~~`X,:?jyzԷ5,T.V)nI3LOPOS@?!p/p|pyS'׀p?Ycz)\f j\k0JEO#-, & v:o]@o*jJ^`4h1kJUH1m+ή[(p/?ViO=OgVr{Uԣ+?e> ׬0j؆f7wb-9CC^ SM$|*C*zL5k^x._⽟;s?8^x պib-v&b]=YohP êSێL/: AS.9hz82i0rttrW$+dk-ZV Hhf_#2atfLbB$/r$ ]v9a"U􊊘&k^x/JLw+N[jx@9q((T'PJG7Mv%[_.%S)U z??,"`8:M`^5 z"c\%56AeG#!k`VᏨ|\* jvC0T\|0@Zա3,GtHr$~ZԨT}G}>cK+}gCDt_]%+oD,yz-#%d]&:uz=?yKy}z6i0B~U5Wv e+E9[V^j70 +B%k]ÔW c {*ϴ?O>ҽ<5 j-SUBz*ۀDr&ICk_i}s]iyw[tzxx  )w*Яv#w=ŃL/Wd%Pؘ2:8Y&l0U?lAĥS3mecڥH{һs/,uR RtHui=莕Q{3V}.mkh42?-Zˠq?q&>7]wV `:1y_9[և!^8Zo]y/[k0@ OS*a7UqZv4•_au/I%_|D~ Xci={}`CN&"zDĻOXkBxxL#b- xsjOAl4:86"z:|h[pCx[ PZٞeMK{=C'>PpLs)J4 J8f[7Gӓ3&Cxdcp쭷e0hr%mI:'qN` \+i^d(&^OT/b@D-χR83ki1(ZD>8ch~c3F?X AS#Ϙ^ʪ;u;Sx}gĮ+>'2_gr9 |>SZ8ct_k29j;z}? X.样e&F*Ƹh t; @T$'[i}Ĵ48]M`4m- |Fvqɘ4_.lz;!Rbӥ>~ޢ|ǒ_VWdc lkvuuGC2uiA[D ,Z䮗5HZuX&Ԇ@1 P_Z]![@< Sƺ74PjXʋZh^yS ](Wyst>8~PoiyvTԨ5fR::bo/RYzw_|Ӄ>cX,AJY9tI.^R]oc'{3㏼~xō y]1~d%*nKˣe8kz @;0l/ۧ5%Uc8ף̡_壖HErcuaɬVp*X'R,dMx%L~ ?C7ٽ1Q>O+V=xp3G_*~r돱b):ײb{~Sc^=[VCN4a2@y;F+::A\Ee} x6 rT+R>qq<..l_E-ҥC˙LNE*6J15sw^&҂a8xTWԍT /<,|KQZG31awP ux1u#:xd~Т%FJIOHQǬC1Qq "爍"6u0 j)@9Q{¹¹}lLg ;UOh]SL"׍Jb _d;l6~ b+-ʿKNnE?WGGD' <$s~qO57OhfmĭMM,V2L/5zQߒ?AC~ZHv$o> q&N@_8:;c yyF<;ISp`#jDL+k)~'@5 Wf~> ,hޗvss}{OcQ56eΙ-Žݣ qh\m^.-KswQQFeZn-WQW}KzeMj[uQzQn-Ũj%Ejm%©CЕcXS׽K1`5Ŷwud`,[l wR`q6F-.\{pJ`\޷C"C0&, EZm"Ԫ;U.cM MK4PW7do$ qw ppvJ)p:w0rC2R岭XP]+]+yD.H5lH:0W~6<-rj!QVl,v<@4-VC{K/س 7MbDjI_w$7o<(K_> YBܢ,T?5wA<[ͪ6 !2Q߶BpXh]68Fq=)@|eNo0 8[0/0k @;o2B/ HT@!%"nxG0M ·=ΜZ-F qB.T4z-ŨEFQxZ;DQ^yCfoQn-JŨWx]jG|KKz[Z.j h]ۋqÊPrrsUappਡ?+(tv4 -/TqlDXMcס #xVE );.(pfY,.i{hثlf !.dv/1ւbԶngЀ^30MXL]WiT3$L@lh+n[6B%{ (?$ǀJ k w' ^,CĎeLIzE~v&+lB"`rRռH_^s'%&ЭE"K22ZbvXKZɄU6{r:Sxxj) [1FRdT0 >)aTiZqp"l(`n(`Ad3Cὔ9Rd Y*--HψKnq> w 'Hb貒ꁂ.N1@uw킛PlNNmG3$ŅFR46DQ EԺ+0)jSue%f" z\p|<.9. _]w<@'QWĖm<M(^[F@u2Dt3J[E":qXK8qkynkF֧AnQn.DEDET[x׉dQEZ[օ.qoBZ-Ž,-Ũ-9t(Q( ]}1ssmF?*_~V_B[Ei_E |)CBy-Vmy; v)j,qJHVQqt R)ψ j,) hZF@Ř)\,؁+jfb9-a\hK(Q[=],OB+* )mg#b c%ޕu1gu8Fl?ϤJ7ҋC0ZmTYwKѶh(AOm@Km 8JWhpeSvn%sD !ڰ\@ (aWev /SޑtAa=NVtx)V˴s-Q{T'?\ ݟD[Ĵ<̘ !#@CjC6g{;r/ }^b7+Sxg ԫZWrAmJ*)34[qikwBvM0Dl`#CGc*K}6iwXV*NUU(H9Z;?$_i? ]S ER-{3-J/&&wan]EExݨE=Zx۶j]։qoEUYzŸх -oEVz]E⸢qQj-˭ZK^Qqj9q\S2a|Fi^#1Fu%F-869JXԓʩKGK6S$bHfs|2ҦEjaDB&9?"3˅0Drb[, >\ X:h A Y̯P8VbzDnwEnx&k/ (3sNB]m*?O"/lQy^Y,Iٹ[`oP6jT.Dat*ޢ,cQK`9W1H:q15bŻ|Ke{nmFML^]`o1@/۬f2S+1X` savsF)Z0^F1emhqq K6"˘ CBTZ-zZw'j%iNbb[QEq.%˨ZEqj6\uf,=1n8ry]BQzQ[бj-EE-h(Y]UhkSk92hS x\ևS(9* f KeYf_ZĽ$s(mKj*e !wO"*`y=vwߴW*l-` pE'a9FZ9u^"ׇ^RrDqUu%)^"..˖IW7cq8\CYY[\n\Cs}eSe!lQvmG?iL{K:/H 0-гҁ4n6]a*HŤ-S{è6̫Eb˶-y ͞ȱ3f?gh*Jm݃I^Qhw;o}QoG-.-E(qE3x}:E0`֋Qn]K6FhmS}>EZQk@b[qHзwoAt]"Q.-q[q⣡vۉuˮQj-qf*Q P5$z2ؗyڗOGq:['RoYhX^XFwhDY^xTPs6$ϭax)fª6zS06n( 60·+T2~beIV1}`X,)EV1qS爢JVZ Ve糫MA Su΋%iM{٥,P-ڬǼ0 Gu\bkso; -T[+Ey?,n/ |6Yyџ%poO1gi}X+[GZ*t 9pEqkx ΁6GxKQ+FZ(wh۷t .R]EuchU^K|Ÿ[зFXPn=i.:Ex~TK*.7#^"ElshKणDew%JNsLe/лQmWbң#ڡ.2pTeeSrōؐ& 1(j` ʳdقiVUz,<u`{T-6Lmv!.k6~*vn0GZRwITQq!괠̮*ĭE3'hRGP[-  "{)=;ź%jQhwTYk7F/dзiUQn1oEYnކKiu%ކ{EкomS8طZs ]UqKŸQQoR8Y0QoМVqK@pQ8s7KxKAFL_14[ PҶFj 3ӈ#Y Bkmݨ* mkkmTX@4usYaasVJo԰W*6ohۄP8=K%~ x`~(~(R:EvK|tc{zPh1`~VIw* Z_䫚K6մs.6;Ǝ8@HKW_l@XgR xr %[0yI:lm,tP)NډȬ%h"pE(ll Z͍Oc0id,v0 U- դ t;'OBVzQu,ux׏Ykxzt]FݢTS۴J|koShb6:E-EQFa[Qn8q.^[oEQzvQQQFEKͱn(Ex : s(*88l`uM *8 ǘyXCzV-)RWw,tԗ+FW 9qv{vH0,hA`fԯpd QlbU˷INGvus!B/ 0Ȏw8,_ j)RC@;E+C ¨ʙȠq* ,-U鵆64†!mH@%Np[T0mQyqYV7bQbVIUX!š6-wG悌wAXRETeMnT[|qo:=иVs8Y7͡uyEjZ͉U#d |WĞ35raK*E.ޞ䳴1ΉwwvZ]h[Ǡ{蕫h+|E+lĸ-;{cZeרhs#-- ~梮4smHĭKQtQ(bwQ"mK*W1E\Xqj@[B{-cXTVR517uǣ@&}.){jxʸ8pwG[EcgTXU} 9~FZnZAq{qR*YRCޛ),bG[w\?h%)~A(¢ Kܦ&jD)R R>q% 3*BGS?(K$*[7U6a(X0 Fd޴[ . ֩"/=(.c[fV=D@5,7W F+C QDE'!\ #%;:xLpCwh3 {_ rGnw-6'f1{v}P⌿ zxjaEAG[{þ|hE1Ľ1xZE] ~4aG0*-- 6Zb֗1Ts\]-E1bԬ\Whbh6n-߈Bq:fl`j--˸ (n WIx(=e;J8LQT-g@D!)[K! 0"m](Bw BFo5k[U+kx9^,+j--3&\p* qAwWZ2=?xR|cQvUsn=UҢDQwX.ZU-e%=u352 Y/ dV"g%/\%#* ˦f1c@{N#5[|G#5R^iI|D`m[y)̕:sX 9ywlN°BZ~7D昵sp2‰\5ϓ+"5eMxXmÔdBP@r=ircn--Ee071]^q.-3-J0IAH,stEQ.;-E"\qVt Ĩ%E[f-n,Q̶%E[qEw+AEƁqn)D/EfԼ\|a[A>DD1 fS[;2z=^ˉ5EC)܂gy:w 17+[{Ni(:-*⅂h Z}QB*ܮ n.YoNrfpS0N=Z< ;= -fud}(Zf^_HU渮Er?!0Ӌ3jcRT۴⭺yQ|VumLXUAU: DL:^ g1:`* Pg1"5sCrOQQyl#&9guR/0Uk 8@gc,A =~Q`{[6lk! UV"U"o9,`3K:v<3Ozq,Tl(d jniyq}=KME^⭋9N9 ZWfI|JNZx{XH䏄2 lqCvG; jZĭ\tJme:L6Q㈷]zƚ-E{t"Rc4}ckE@u·z*d@E[h-E-x4(`NQSoyŸQoEdTfhk.Y#RtÔqpӵfS `l0U1hXy `Xd^LS񀻅~O6 >?a TfC._V/>TPh22&22ˆ;&[ωP 4g pr^=$'@ؽݼF_%aX$ؕc` tWblbĸ YdwnDs0{bg]"+.3bֶb:S["FSP&հoX{0I(1BvzL)cd 7Ջk{Jc&-.?X4pc!V} Tg4=՗~+ 'c(=+*ʼAx!_l[ecu14VGr*;h)` Qm'ck)_ ǣ3-y~1TE%xHtG 6X9C#G@NFݠw;1q4-J+bTsEurh5n,i]šzahZrBuŸ[81n-ENtW#M qoIXQ\Zz7AqQj)2eU9ѝpRs&8^4[6Tb2&4  q ᰬ fۺtM|Pٻ~[OqK^ɨv xə _A ۻD4,7Xo=7֋W˔*( ǘ jxXX}߬)?hH++@@>Yr_)ecm`70htElqP (װ (IIܠtn o!^>+kXPE{r^ h۰2畬xS(;țdi!^=Nh( _P+{/c\/usyK 1WaW݉r>`4G8G aޓON: 1Gal^L7aWH$ Cc&"2͚T}Ew*0XP!(m[aH ;w^eCc#m2HkuQNdme-Ž"]֍N.]ӼoUxDzZ~-E04Qf؛ĭ"u%jbshJ(]F-qZ*kBxGPQn((,cDy1K^oq{E#HŭKS)A}J4p:4ԲUW0nc$X?A豦{%"*KlA/XZB+iMY%Cg+5L7pz3Dwpz(t1qj1295v<[,Lshl0 fHQ(_R=YeAP7o[Uk.q[2#rV:e.HH9BaX}- giCgU?$J^8""iJغƯ- q*%˵98g~'y2RS%LOvHgoZZs-cnqhG@7EܟqmJo78[@(j½iJ"od|ĸs#-!uwWx#"VPA` 8NQSN[܊-Ÿn;ŸMĸrA0Qn%sCKZ[(Oh6aD$buK0kBX/i۳(fU,ᨐLЀh߰\[:m=&g o䵼-"9~Uh 6Ǔeu tf3% K9bH|- h+SoMsv@X+nƒ )M.xHǹ(If@{S1%8<\=8T% ۠V^`0(eB _FYU{SWdFJ|.Hu}`PdT -npt*\k :qy"4vpV@6ϡn샺mq1=rf,u3wT~Er?d7U~cLe`0F>qT K@7&?fK7ˆ9El!CxX-vsk2?}(Jf VhDB#6F¶YxSXS1C%4У*={v9q:X({T-^KepOP<ekP AB͠X ` 4$dOTJQڠ4/L \[Dׂ[[ PbP Vm,뢋pHGˊ ,1gH5u$@?RHqQbD(I! 0R=, E NN&uS .3ZqK%m;z6"HK2h,7T!2-|+,Xw2FD6cEJ!1QGHiz=⚺uy\.9٬ X"A? $wA^!֨aS,)^#K5wXT6m<@4H9L`2؝`RaZ ~-Gk$AZk&<ifp^[BP#% ix-m,%vu`PZvRۍ9KV ׬P4'Aq /Rg_^E ;c Vأ{nd; ;+%;pVq 29hW"Id*#a2.D08)[WVU<,i2G:Vq@!O}o=ouv:4*s]~[/M{ZZ<qh*( f :+]\I`ՀV=.6GV_O/T & .M v&fC[УH`+3FMOSbrʿWQ` xWJ=ۊ:^JX{ݕ*‹Se`>=ϰsKEu[˼fys3nt~A7&rKlCrK]H4\KO.a|j^oK  ݚg/߿^?#5Gymțv-oJq2;-a2pnŬ6'"H{D_rp* qp3J@6iJq.+F &g;GNY ?q?sto|`*{mqh`\/ldQUl&;ءd^[Ysqo?a٠R%m)bU䢄Z4#D$2˱{pǮ \JjK )8-YۮW1=TW̆b:ϱŸ3_Ih#l j`-rc`f "1 wn"L62( @P 1R檸E"hUa3/YG!uݡ[Df=?VF.bmZRRy߈Y^XXXtp.R#l,v\TmKdJ-=:b앉K 8?c#\g~qWx=ڬ37iukF6):vH4HʖFnCYCq 5]\۶H`ޝTK%n%x$J˗U P޲hy(F@l>/*:gB}BB% 8 +Њ(bH/tZ1Ay-̠fGAl~of+hNST'̔?\<"X*X1F91v\.xNZ\r۹2Y F"ԯL-y9(T>Tw5vBl F4.~ y5|P8 mAqdT M)mr9g4:{SnhAXPGſ]`rcBmwާ:X1oЗhB&o PޭP+f)y? IqW{W(5gnc9^08B T=[PUpCBZU1ƨ r4mE*V9(D)bvbstr˯xe]ZJ^zB%Z̡6#3[ڜ9]`KW2kʈY}є𔉋 mlSv&VX!u1)+nxtރA÷INە aW&%{-s1+=>eѧ)?V6 O 4P.vip^_` ?sM'yDx"[!v.t V)TFLIVɱrˡtXs)t.nH槵!IOs|Ũv4X YbuDmx`I,y"bƚq15FP.z@ 31'WrQ:WqDQ~okc#KNCYKKS| ,7ʧCOKOMAQbvEÜ47c,g.V^¯آxf皼Q h'ǬsPqipۤYds=)p Zމv˔- ʚ&X e2@?s'Hp -aDqS4 [qn1kB)OQ>@Dc ?+ V )RPI!?'w%J?֗Rwh!oyk$HG "^/?<zNgj9ax2WlNRVh4V쀭1bȔ5r+2#-f?}6Hi9ot|opvIO8T/2) :aH'1 @@B0HGJ$Yf!u z9ɓ&DN^_BETrvbzҡB"\ (a 5,rK6:k:J)i) NA O@pB IqÓуJq(F%I4|γk6.:4 ҃Ni=tqI˓LK>9Jy- -4IDd^{u+7\j]]FX"A@ nRiYAUoۮ1ARŰ dy2Ƈ+3 -vEl wC)LlTi:HK-@lu\)v}M R*:m*PUnRX3hW",P#K̥rd'̠t[^״VA=} 䊣eyK{!!2\Awj?lR(v+?1 G*U1}r5 ,lD [gjq{nyk.b$6 RFDuA!{\~\ۚ}&X\`z24zQtzx/I ry&QR2rD4ĄqDChΥ(j!M! .%B_/q 2ޒS2ٛK`E SD HHa:A \ ("Xg̣יw⾿___-/r_,~⟯)Sq O}q)>[?OZ1-A1G/oZW+Oܷqo,qo}?o,~ePqy}w})~߷(~Ϸt7>?-k貭m!R76 &6F2gZJ#ŀ!bI,hBɀLN-!gd{`?S:,nDM/0C7~4F@r ߼7?%L\H @X?"fГ/0O0ŀ)'wH>JŽ|c3r_:kFqY7ch(wMD_?}:alb>NOPOQǢZ<.֍,_}!eNQaήŚcAbϕE,WΖj@} ,]WRuOm`[€QDgC!%Ͻ0|'i登2z]??v'4.F` "܃˟XRy-7Hdȼ?`$gr t>9T2';i]W f zt/&!GbzTWb亐KS CD1ym(4;b,Xa_N=TH++SbOp2 LR`Q3VO#i78@䯜P^X  $S!%P fl3'B=ECΥ Sz> xpuQgiYoMzcAP]GX EG_}eQbQ|Q0Ƀ,mC/N&npC7v}>zexX?2Pe{<|l'T$pbq@KDo n3X hE,D̬*(z"U*u怔e9 4 yC.z{a͝0YQnLe1V1PТͣ1"i컆{RU; 1CI[ifCUXJxkX n@Z@}[a4@s+1P]j+B̃Ej5IwW;#ٽ OM=ł^;!fP,[ 4+Ԁ *ψDX_%F5TpN%q"2xNr\沦W7 A{Kx'j3*BRmmP0Pջa8dH>W+ <Ա{jWE>GƏ:zezQzKGB`,>4,_@: /cǦ,y2Df1tyE?t`r,QR!=A Ĩ[ T+/B̧DC:ѣ~߻f gZ]6?>TB7N1q Xׇ=I_%\|ЙYI&Z Dh瑧r\p +Xe5^**@{QLBiFV$ٶwݙ63^{+*Me-pt bҀo fKtU5; /pEgqH:Q[_xෟ|} :140,b\.E4\u&+yE(6֧G@=Wo] $+j HJP]nX3낅UJ!nG&ES!vЂT\Q3yhԲ{3$oJe b|Q[_U.$}W%G+f$_E԰5m.< )LJ"/} (X/ b>4;,XweC@PyMDF%GiX͡7A OQƛbd?2~2Uh9CpP b RAH[e/Ϥ6˾ɤB*#riMr%iotgvDt'k 0j2Mʆ] UVR\pnP9I B)D:v[d h iWU]8Axo(V"GVqٳ+JP riF69H4L5 PDk۾B adD D6vJ_2nV|8ʢ C3U?n]@\#բ/j@ a3H'h 7)KA]XnRLwvNNԥZ@fc V5)\tsʕW~6%.hq'|G<{+]>-V%4S8}ŎtVh꺼#΂B/}-,]WE/,Xyҳ1ɻǑ1E2c -2U}ݦ ʃM/OvWpOk shx/ k-(VOcDY%mx Ӝ2/V}p_C);Ka ~TJCE&9 ]G`v,ˣ ynjv7wm&Z_z 0mx>!FLZ@b@ NuVFYS{xV莠 {Hx ȽEZv:5EPrX + i4ˮa֊v!;P 6UV$ DU@[n˵Y}B 4Z/cJ璗W鍴pEhM~#L d Cڃ_mHTwB.BA17.?2'X4&;rI7~ >Og=޿8I"׽$^BTA 6FU ΊVB@U::",X=bȾ.A>T]Q}(t.y=΋ycΖz%F#<ǝI@yFZ3g8 j!983!lFfվ;|Ϥ|q5J $_(*L WPRyEǒ"vrŀ|.|fXڨ0}sY !F%a,$%@Оx3ʣ:B$C/e:_}VzbǝC΋gzW8"x^!ic^Np ;,dQd0D("t)Y@8b.`cٗi 11b=a|'[ʙgHdb6nQa8{Gp X%_\?Gb\yP=`G}g;~NΝCA|DK4t} E녑tQ/=gu ,]cQ.hY_XP ,ad] u<@"Va%>t"!>}}Y,l!Y~"= ض XaU![BT? cXRl~L 70q>\{A~2 H l;b\/qӻ+1G*v0G,B9++Л[i|oÓڅG -6!;p@DIH4BzE/,evDSGItXzOFthY,]Y]K f]>#ΏOƅ/h.Z/_X6#|i<<6z FШ?*yJ4}МC) dW7bOĽ> _g8 m 6_LMm߀ah/^3 *NW">K'd@W0P JB&߯a8ʊ*0NѢ_^Ac_bgNC'% ]TdH6*ul"sqS(`?zjBqEE?@+.D8"_@ R ľ9Ɗ=t/8=#Xǘy:8OO'r|bOD~g UA飿 O +3UC n#SiWnXG+VYPR$` T?h-WgH_m#~%(?f?B CW@Û^EEhVhҷ#dXZqي`0~qIKe /իÍlȈRa6Uqbp e+<,}P#1u낆&^0(o`j#tKtJ:(S S'knPB2&+MzgBu.<Q<ǘ.:<}b躏1=Gdt>=&K_q(P[*?x=p eS#?HA>7~\M1x*>Aq& ڰy00 r/w)c XKBᥰ L,HԔhg[-VasWZ9@8ܝx-k1Qtmb+"j@v "@x H_E.g 7Cc{Aa<,VW 9-8s% x/ c K ]JSNOBW(eM~Oumg9VPY<|D`DMk 8zp@n'tz}+tۼϕꞎE]5=:/@yyG!fCΕ|FG<1CqΫjCz0KIKNK5}ĉTC1Q8iԁv _ku- @wX`iVѳV_ A V3I ]"9.bGڸ2YS}-]̾$m {-mO“WxT #v096C܋88'Ķy-ʣ-QP =q0WpEA V\{c/m{~ ,pI ۼ{`4~R+;cCԀvcP "/T/uSоCUA}cK +Qг0iFAu]+:t_GtuztQ O>SyQJaJ #.sgV3iQ̡{¸pQ S?vK?v@"{@T曣`ʽ똞XN ) a4v. QkB[^7|NUڵ!Ou˝_7SĿvF,!Foġ댔`3eAD0Qn(Vb:2Am'/>B%! :ʠ`d*E]ten(R-% 1īBUŊS "iyvUCN% )lnݷNcu"/fb֗*?]ӂrњX?0nVfo~ET8U{1p>4Eh}u!}VjXVhrc΃X> *b&3qj M G<3Λ>}y@,8:M!I$}E~(:gn܃iy>>A=Ta<>:{Uf﵁Q"᧫NnVvNz>oaߨ6!fB<S`9fA}b7&}\7.#*2xK3q?[M` f_( Fv/c#`qP zL[;6<]5xq( ca. AĦ 0o G(mMr$)`)*= cm;vxS X zn•F9U &w{-{`ďssg s2x0&cyowm8m/&7 UoT"DM{iry2/{֪"[L{VZ88uJۘMXN/5Ac-}S<]85_R<ŋQt>K΃|j< >5A]S.ǑWx<^C9: ݅H?F>F T]1+%GiBz&Y8:JA7Zf^n“CG)Qf Q5~ X(V X [Pˌ]Bl .-i3<1wnXpB1/{UUmwY~D•VP{ᪿ5/K^wz1vE˭y[4 KHX:[&=:^=_gof>+ݵĦr ApBϷ$91\BiKtt-蓡>4]U} .><}cϤ1YQ}@<#ckAi3zUPe*ŒٴD8$ѝ0vۯ23QBao=jZlY97нg'+~w3YG56* 8^.tqΟ7x0s gMi)_8me5l<5eZo#AJiU<<x Tw0)RJ-j* 9DɻZZ;ŃuQi+TLb0~t|"a(N#9\g 7526:`: ʁ&`´k>a@ER $4pr~!Py4W\"Y/R&#vvo;Jl{+0*dޥ|}c r#>!A\)jHA$πmޠ#h|$쏎\]+LXQ'%D4RA ׮^0wBӇt:;wzuk>]B愛ˬJ}"*TӦt513# 0%?D_Vlw1]n63}hNuY2c.o_fdgBΘ)037.I$Y7JGidDJ:o uʀfw<ǟHtKu ӨW/%1@7Wʯ*7g`*vhYD}^c }| }Գ;JSpwn2RDAa:h)!bQhj0 ]e>OܺL go`R^[DTUQy(m0[_*[s^od [B7n2VäXr="&1)є{qX15+ C< Z1ةڟ2׊7Ӌtxs0ʼy_`:Gఛ QQ`l;EzKE+[}m?d6a;Q7 pݗ|mҶ[~P쌸.ί ۼsU0)fQofU"epGp.mGgzUiduhf&[B!EIne#QAcxF2(hzb  [|h";~mC` 6 !i4P*ee |Y@A"+H6j/в";Ӈ{?KR..Ўo 9>Z*ƌwPzҕh#E@O3- vзAP #e k >/EO:;hJ]eŽ㶇ƥEEESϨu'u]e?>(WPf~-~ mJ?2~<[*CuS*!l{&M']S.+ڵuHe9 T)nWZD`"ҝbm>S͜9%-9x{.m{L4T^ 0{KЕzu#<5)ņx\L0j4*򺅔/0Ը#_-;@.#/wLn+h%jc=;i+< W5dbC sVKr(8Xs[0]WX Mmw*|}Hp{q/`rGN2#th~eFХ^絒#ޅ8i@)hzGL ᴘ[*pxaV,~DH+E1#N7hDB#6F~z<)>gccSgOBXQ7X3 ALՐ u udD(դo\ dZj0rh7kQ}hEi+9"'U RMh%mUR(-˯\n.eOJ0ܷNkieGHU_`΍ZB{B]J~"1Z\+sKn|%@mоL>ɓ:n="DUK>1uFq{]`;Ahuv(yUX wvv?0 xM'uG%<9Xf_tlp@n,sP-@>jZnSrN9uyZC$(l45l9WK%Ɛ !k3apQܳg>+ ? P{{kUJ:$z$[cOQSDDZ2wRsW wXm^пGw|p-+"Id*#a2.D08)[WVwt.9gf$shjẋ7WisB5yjjz َpu BiORSaр%ĖXemsQf2@GV_O/T & .M v&fC[УH`+32iOXYzrʿW<DzGZVh9"v%T&Bm% 2>6-,Kv-.ٝҸ6h D+*50C0Tf"Uۣ`)Q/}-a;0.w+<Ux}ʛ$*f]JJŋq_fz [hoyNi@AmgijE/@M4v%Rы6{-#viSi+K;,yNMr79sΙCΣ]_1A5;%E+:y.9+4S` TrpER<(rP5 7uZy`4M 3efT5tH58NG _a2i5; 8a1u%4UMwWb ~Rq_0r)K2;vjdTe4ͺFNop-hނ54̓R)WwChA G2BBw miS Nc-#%c/ Qb)ٱpvMZ|~aA'YqVq ]it2)+N^ "/5Ez^x;&#n3pj_xtDR -C˜ {U uo8<)Bc!Ui ̀z#~"vk42 dI%cp* qp3J@6iJq.+F &g;NlooTb(^ȢVLwBg(S; i*ބ  St"} Z;B(\ǂ!#aLB?۸ȓM@N,aӨ B{@TiU\~j"FDOF@ߝT$#RGmZA6j<;U~"5d]B@PTomK\A^@ t ]{6Rqpl-2HySml[P Ű"b안:1FW,$N="c ZeL_Ry*X+{K>)> ev"=D^|N%ʭcBa)[Vmvmجf C?"f?ă LvCÄ/ >yQ<]AZ a83JXїn|TV*WGq_[Z}0ɍ~Pm%>5%$ݹ{AhSUp W%/OGdPLNU?={1i]8-v@0/y _`|[[R%J!&v)<ٍ̱te=qKɻm,G$(*A6mʾXP;Jp:sDVRV"fI᳍dd-PnS;K/[*0U|fZEm6AiloWxJblT_hFu{JA_T5icxœnLycUq;oAk'|..aOi#[EJOa_<t2  J3dJ77M#l  7n&qj-htRx0frfy=ؾ➤B;`GU$Ɨ "Q@XeQbV^TJzP(GLmP ,UA<;km}u` م{-<9<`^>ݱm`+/_(9PB^*={ qv'i~/$I mGV購A> v?8a{}Swmbn'#pf?Tz.c3DH_1gK *f@630V?Ȱڃo2ȩ|7VS۲giu .XPGſ]`rcBmwާuZQ(c 0H7T "誊hj^@rtGx6ӸCeƬЌ{c \FZ<`["d'Z-thlCrVe_E_S\j+3UbvZ+|d"Pt1J1y9^9e׼۲cku.$ܡ?%誯b0c8d{S#kj&MyQӇoTc.ѧ1Cj;n9(;Q7#gp|{nP_2:ڜX31 Af=:K9i4ٿQdrHlR"רBUm}N H+7zf\2H;ar(!YqwtJPU1ۉUYz aXb;B' qYGx:#&)t} _xj.|sݥ :0pwTGm9sהL-۷yǻep{s69rewN%L;[׍ۃcN%&ަ̏x_nʊ^zM ~\B1\+C=G7Ը]S1x!q[x WY*s-Hs&Q}m+4U&Ja &/ZN;#(D3 ezcP;.]$JNY. (QSVe򿜾 /7lO!SG5*6 %HrYn$1B\yê%Shl '@Gw̸/@6`(8Q肩y_} hJa)̩-Sq2]X @K z' JJu}5TIIH+g++) YXXtxi({YhVUYI3"w!O+)zRX!6wYs:>ɏI)v%:^/|t٧VVxKhDcXNDCH]i{:8{/b0dXʗ^SM#E@D.]h$ LX2Riըα>dɔf؝" q<8hÃuRZ*TR'N R°9bKˬR^$}'Yٶz[6կGnK*Yh9ZBɖgNEݯ^zגǟO~aW_]}JȽԡ%2e\r? m/am:0xe:RZ| fBɋ*-*bd5Q Vcu&Dx6A4xDq`tn!R;5 G OPQ&I讎VB(bB,zܰ|n-lJ_ʩHN͗ XeEv{SCvԪe<]B\MӝU KS0*Yę[AV !oK 3AWڏdv[p8.SRY rfӃb7+a \J3lR=v%l1eOy}uphe[9Е.Fݠw;7ı4sr[QĨ8Z4 煡oxEm[-E#oBlShQzn-ޡn0^.9n-ER^jmކ˨9BܾZ,wqoZ )W8mwiȠy7srNe+/k*]3 :K@2Lټ6Ex%G p2et Йcx_iWXت2<ƮꛌtEup*Q\3uRVvF2B-=8f"¡߼Zڥ2p3v۴ 2;At -ª{~6ћu9[[o[jѰqڬ!Ƀ  L62({ۢa%%}7"<7Ĵd)'(9Q7152/tN[7$+]Q@2˲' <峊 .;}llC+ i]u; _"ߨz2tbn;}Y[hq{J7ڍ4^#աkJ7i-hTZZ][q./]Z~Zqj9j%h[ڊkxQn+/Xܺj-ŭ3*\]Lo+kF [$nR:l7_Th "Ӄcto/ aUhI<a4{ĸ;j2F.)tn8gS-*d]AތD*k|#maPs,)[\3>ܸ\ [tG&% &ѻ.!8:U*jQBvZ-2 uJ5~3r . ^;a[/v J mMPGe[o%ibnu%ETv.VMڲ{H6VZ48_s"8biBVP 0cxB#0ZVUm[mPF{\:lTPki*E摜iBp;8t*f9>yݰ?,r ZuX^xf %$n%صns広JTyd8xw-4UbQl(d|ô=JNbeT4(hQpIQ/SE-rWTTb"V-ňj`#|s Eqdhe[4H5Mޯ(4wvVtŨh[Z(wѿ1n.TZBiQn-Gx75ZF2lUS܋ JEօ - ~.n>?1E}qPb2&ޗ2˫ ϗ?BO.ڬ6|ƒ;yr-W/38M*P.# Zb6A(6VQ ?uk̢ w{CkA ؊h &9db IjRJPa@ ((A)o1W65[K``S0-oJdq\lu}xFLO<Z;UułꃘH@!֠q+PETv"w;3@7a]wa,' R hQsFGPRٚѤr,p- -J۪ ٘ Vq^JӋ|[TQEuB-Fm&t^r} Zq*oJ hPu8]qދzqq[e⣎R|Eu2UJ Bs㘷H* ŭJ:wˌ &0|6^6nrp.`vp"9K & hmg2@[E[ęX2]: vC=z_h6p%<RZIEz*]du'3fo%@ڷ PhWEӇoGzl !FX/?NX^ m#t|8c.ZYf-3kLbp^PU7i0I{u nNa5Q]h" 8`z4ŕAeƻ./؋s.`^' Suhi<w9yQn]EZ-ĸ.j+ץ-Ũ%s9K@Ÿ-6]Qw- QE[qFoBŨqZ*懲xYfQwqTUEMɢ-L%KqsZz#-L(lLP/Y1.lD1g}js>p&K-'eygJ ,HwpkR%nGۑAV5;ьbclgp8jRZŨŨ;6@B_y[f-E%1+R]"J#T}#ER;QhW1EZ85z(ОAX: wMDP _Kaī.t3;2%+9*:/8]QX{1Y3wybN_ۆju==\FJD:ԶP`JK,)Q qJ_Kd :EDoX@'0]3e.CPBY:_\ lN%|JA@gXTU\Z4f R)yNQ V itf5FXT 뛭jJ f"ȵ 'e: b5ND!arjܿ1Lb\8 m(`m)[rt]3+snb#m\56{H ./0 5p-]PWm%6+Me%z'AU[#9Z(8#gڊ f5q# bQ7ߛ+ m#; {6uVU]2KՄung J;2BUv<6דG ;rcn--Ee071]^q.-3-J0IAH,stEQ.;-E"\qVt Ĩ%E[f-n,Q̶%E[qEw+AEƁqn)D/EfԼ\|a[A>DD1 fS[;2z=^ˉ5EC)܂gy:w 17+[{Ni(:-*⅂h Z}++ye"7GB\ WeN16L`~cp;A^M"ۤ×L4ژ,:v@/m /l ް4teP+խħ ,Ґr1H(#z\6pG9%X8g&Q^eYvl a(<*U/Lѧ PNQ USoNUx ?"Q7NGF AjD9%xu``9cãKyY`b2n=H5_+/Z=r]=ͽ3!L_by jAC/1CvG; jZĭ\tJme:L6Q㈷]zƚ-E{t"Rc4}ckE@u·z*d@E[h-E-x4(`NQSoyŸQoEdTfhk.Y#RtÔqpӵfѾ( ״ʾ.L*TE7v%d n!{;o)U3 7U jn#@EE V)P:/lħ+`Al6` b=Лr'u'v<+59{k_amÎ),'+[B6?f_4L)HdǛS&?6|OS[Y2Lk&.Amqb(/ COѺd̾"C, ^ӿVe|:B&:GzHj:SKK{z. 3C[1*5Jy:u ^?d򼃻:nRQD f@@¢l{Ƕ%ց⍻@wchi[ЕVĨ8r-ŨVkBXӼBеօ1qt(qbZzlFޓnSdʫs;SḥfLq8ZhlL1A+e.#}(MiP+# hnP|99 T6k"*#N"*8+eE@ ];`GںǸCy`uLiWLb]Vw.EQ!i`D%@{*.)HMx qط!Bj6kdL$KVJ<n,} p .Sls)(zȴ:h auFOni)Z:%3Q: ?J۟h<Ĥ%EhrZKH$Z_ƼqmBJOb?͉OLuUS?ψſ2) .ȈT&?ᣓ"? G'?xˊ\w?'36g$X|1qTOH'D[ @_'ob9T:n_ Kе /hQixjeH˶wXa}(Lx{򱃶+xT Mi~*/Z8 ҆6= BGv"[S~܈K V ntH7T\?-i;DL ncQ&^Uwm PAXKNPl=U HKFݛ~aalu~p. RQأ!0xJ`(a"#ٞTUJ%{b%}YM|tp}wIDXWPF)ٗIjl@\^>C?%sds2m&o'˭Er]g m05ڑÑ-Au4r $ LzЎRvoa+#Ut[r(]8gZZ#ZZ@oDm.0g'[ax/a(!.'D]zŘ?}!С9dD*'GϮ` FX!1N)8Y tluwZ`;ͷP|()@9h;!$l*:rw0fXLcvz|zAŗX*1GTUق ADWMBj_ )x1L.?h WCK #HcUKKqan- l(Zp]bɻ/@;)鈻)bT ]@ym`.G\{4nÎkrbXlvYuupW2@c8r L[IɜEF2[!tŨnȬ KE\w,T<,X-r'GSN9k*J'*V!֔i׎_Qt%hǖɐ)y4  wZ]mau䇘#>/KB-EŨ8F,wj.'MApWzQj-; 1E_mڏ.X˙DP%Bk\qs ƦLpMPi`:`mQRt3|_;.;imqӍOIrUL\{okEcr7P8o]{@(ÖiX^mwKZ6rJ20nVAWB"Y G"#xFmn6/tARij^+z1HC]y+Q2udk8mIzXq!zD?7|#&@BDD􄝂(,XsvДGfya#: ț`oFf`˫J}]aJb"E`7Mlv;4}w*}x|tyltQ~%ESz&<'X,yIg0?(iиSq柸0Ap(\"W߀e:2.̫iaA>gN۱ir!xQxAPN fŽL]T *@n EpV@9/U6{P!h%&TJCBNQܵZb$@wjq#b9M(]Bp={鴉QoG%ʃ`6^x oN}Iap`v6oۮ-[\ -fݝڶ<;a [p'ObUiw^ww1p%EƓ0-,J lHbCy(@0V>4_!dB5j5?<ŅYg,MKCu"JeC]HEMzs6+j4DACZh -v,7G-WSoU&=X&.(-[@Mʮ`lfe;4^I[/ڳrF=DB˟0&'1XCw_ +|&u5Plgw1qkSР#V\:%PHDQ~QaYV->w@`G;:Mk2$dU LxƢwklr;K/p:?Y$`Z\l0r7md,Sg݅P dfm6BsAU~ AO ~SP[*ְ;v sucl`.lƭ*a[1AxJ?9CzڡuI_bc~k/>r7~Gļbå2w<WLڷXKVbSͳPc2bռ!ͻwʡ͇e Hn9u޳q(e*+%Yq@TdŨR( LJ|8ĠbaaƷ;+B[:=Ɇ([WX6?Yfsq;?) R.\Ph#m GHǦ-A[}j(nOp8qU$y^n9nAs([c q 1QpJ mR RY =a/MM'Lھ%M1X6ߟt:_ \1a]4sml̸mĽ;Q7|uteTz t|%-\QCX@(#o7%Xns _AyWQvNQ-= a䛦rXJ򵨱qsc䛷0qlwVjxmly Kea:f5jU|Q;tIO̳ pN+:fRF5UʗIr炻R*tX~3q-.8{Tد{b[ }\zP .GȂxnm;Zju{1V >]\(,`UyT@mMdZ&`XoGŭT0:~5\jv8…JSN2F@R-d\*KJ [BKU H|o oa]l=܍E"2YIhln1| =Plth2eI `.1(+%2[A11B#l+ pKW @6r)2R ޜ<ֈB<̺Gҹz{V 2@bu|GI]’jFzxؔ=ZOD{:sqfn rϱ,@^0* r`+'?\tTG2 uS2/S-Sy3/s1oօ\N"TKk<`^":L`.gkص[㘐r5{uEpan%yE/;?sqc)%IPH3kEI``:|^#8+їR}ʹu {LmV? #Da9\KwoT!uWD,8;=d Ó-.dhvW؀"׏4*Q'R7YAiڃye:f7(TvIu pHCV@.dN$DA5 zIUS`lAn&v؟["nLQ/;,7hg_8R0aaB;l}^լ*G)v: O/ ;H+mۓjmaDGx,meN0 $U-_6s/_Qf$bZװGzn&0V0j1n]G_W84ò[5=c'WJ)w3Nc=tJ9hjFWf~D!h/&v+2Zv,gjyXn㡨\C _,VYyݷZq9%.#g)66M"}TEe16%(*߇JƌL!RN^&[C)K“s"^&H)h(rQoENHhḱcށ·:F~%K) @M9*(q(:oU: hx(&t\ݣ)+Z/Fp`JֳTL-AG{UW{B4e#"E.jRC( _2ƼyhgPgg4Z~f~sX.\ժյ_xfܲo:F`AS .{4\6/)[%V\٢Թqb:Ÿ"ޗ.j-ˋ,/xvѬpV *~и{Ƒrv8.--h[)RY^3#ҧX.o#w_/#|Ky~"|"'}; oGCGG#"Uh7D 6GA??ʟE@?ʕTJNNV+r^g.y؈`sUMdc4"VF l#t1_9' %Smx%_ե?ȟOխLyҎSDdmREPQ!BMI骔 *D^dΕaz詉d4J^9FgXuRҥU[!*2F*cgtcSc$#Vs?S̥r+?ъhb7~A0!`U_DK ~L&1Ëx6?1:jcYN:+7\j]]FX"A@ nRiYAUoۮ1ARŰ dy2Ƈ+3 -vEl wC)LlTi:HK-@lu\)v}M R*:m*PUnRX3hW",P#K̥rd'̠t[^״VA=} 䊣eyK{!!2\Awj?lR(v+?1 G*U1}r5 ,lD [gjq{nyk.b$6 RFDuA!{\~\ۚ}&X\`z24zQtzx/I ry&QR2rD4ĄqDChΥ(j!M! .%B_/q 2ޒS2ٛK`E SD HHa:A \ ("Xg̣יw⾿___-/r_,~⟯)Sq O}q)>[?OZ1-A1G/oZW+Oܷqo,qo}?o,~ePqy}w})~߷(~Ϸt7>?-k貭m!R76 &6F2gZJ#ŀ!bI,hBɀLN-!gd{`?S:,nDM/0C7~4F@r ߼7?%L\H @X?"fГ/0O0ŀ)'wH>JŽ|c3r_:kFqY7ch(wMD_?}:alb>NOPOQǢZ<.֍,_}!eNQaήŚcAbϕE,WΖj@} ,]WRuOm`[€QDgC!%Ͻ0|'i登2z]??v'4.F` "܃˟XRy-7Hdȼ?`$gr t>9T2'6,?GHL^F:d@hCf>?~Пs,$3nvD6$ U̢|ofsST?TI,`HAAd2 \=>LJ]t81%S=2\& 3a~1B~,Y<ĉ!z~?Q/G0`#>_BG Р|yG?1WVlR{?+MXAUa㈾Ec,.Ό\'+RZ-^=Q?[Cyuoʠ%{R8soPDOAu!Y/^dij|*ZF/XAh<]E_}K V.,]CΫ/+/1QY},-CC_XŏZ.xYt>4 EفHфmG^n{Lvуtpapi&C ,V:0QѦ >)Ŷ[!_~< O+Ck̻Ix/2 ~:$aFe<:ka xt@41`di1wg|E8  w#vYqbXc4b3 gIwG.,AlKk7)b`劰ZT?`\GKu`@_cSjglÏNR/ -)[i=1Nлv. @`x@ jѻM nҷ/,Y<ǟIK,uEXGOU,|EYtEQ}>#Աnb,}!bcΕM<z)iif0nT}i}{S {\ ]$ k#@x`K_iB07w}C'5J!/hh౐?rOA[_1K tfzb>RJl@L)s}T6{q;8]T;k#K(*-ŧahQRRS x/&մRn0+TLD$Am m [ŀ1rt==: ;  Uo1|.+R6Rv ?VT6ksZrn)25/n #*ef:Jov~ e<*[I n m,p:ۢ6P* ͶC ]hV)fZCF\ͧXpœvhv$zL Xb5g]>DixsX()EWosnDmsC=X0`U1tK@XUՕ]TGHHEЧG.SEE4:Sz:j,_}WSΎt>H j {׼ٟO`HA84W,|\RB1j1 Nmt;ɻ[Tմ\?̐:cPbKV*NEH&&E=gOQ?:Ƃ>cz]jx(M;3cvA m^`S@XY*=Cyz ؖԵwt$=NЛM=%S%y]^u._*&hq- E]( k2ӝUG `+80p2Ep*M^0^=B+d/fO~HzNTLAn>BWw!P/{~ZDoՠZ`-ycjjZ2ȷ.q{.`c! /wgW:VgtRIPڏ"ʰT]O\]#"E/,p0b}_xEҋAb]G/<躼ǝPN=3y..e9vslvMc2l.fm[GTݒA `u*\y!'`G.oX̬7fe( z9N=[–r/dY-ݘ[J]iZ`ᓭ)}y5bojj!/B9%eFxCWZz|կ5jK5Wݱ6j]2V*.چ˴E«} f]d[SK|q!jBp @4ҕb۽c0_`902$zr.QP5v q ZW L戱aEU7pV φVB+4(H]1! ,$-n#VoӁcJopu eM7+!bЫm7q˚F4rcpdfi[6˕ߐs7ݏp zbVb.::!K%:}AԺ,.yy_@<ᗠ<}Y u:,z\czoLHmz'=8! 0( >GF_͐ۡoРg eMK7oแf](&@ň^Mo*em"9$>'\S!AGلR~$3W`~pq@.8$.*lK6b#Fh::viH*M@Z1n :DAKMd %YX/.\,}.>W(Xb,<ŋtX҈t,EϏT.]1e3 K"TXJ /'A9gf 'PhxfŰ]  –cBg A%qk8~IP GP}EdeArI{?*\zB(P:Αhv _84XL'GZumЦQh:,QdX>/=:/2G |G@ _@/][At:_xm!zFy yl!OQo3P/U3y.|iO 9!܆R6o Lz!}οψz",?H6f@"G_Ǐ}mu3 2VI ]+i@!mƤa8><%;a#WC`5EOgUܣnOS  :P>Up}@:AV(C4WThhvһEq(tZ_yD_@ }t>?<_A}}b<2G._}Gy _x1Y ^OLIH!t w=4w 7%|&`a1|sm!:D{ ԼBԎMAM)I? G|70C!W}Z vL|c8U^WCRbѠn)xZkym ,:T0B'@^z؇ EaP!"wnSy䭖rvhM1]qwu2z%DPLvYhiksJ2&+MzgBu.<Q<ǘ.:<}b躏1=Gdt>=&K_q(P[*?x=p eS#?HA>7~\M1x*>Aq& ڰy0 |ĝ>Qd!!pzpmt c_k~#N%f8GOoa\3.)\a1d:^ZeWz:>Gt辦oQ:WoIMt>Җ<ǝǤxGƯ:/]G/=%._E;m.li& To S $9FcYR0I-}&$Զ|7d`TXu"} ψsd`"Ѧ-cJDNϵqfчũ^[l(*&!jr{A~ज़Rı{a-nv=[ ]53b{rK2kOnf>ys`0vܠǩ Nv^QL+;͐(qu($ؘ->vcwff!#Yw%d_{./Z`np+q7jz](jW/c|id]%vJ4_xt&(Բ<.ǟHy>?1.! zAS.h.N=A) y2j)^)QDe}bm*9/xW> !jgg^[hd_+pn{F~OA4W/<<ŎA/rk]>Wvк:>4yϣv=;v /0|`3CFAquV` +[a td7 q|NU*('4#s yVÒQd3_l|Cf&=K s WH yhwkK.xudb]P勀CLr cUG Ϩ+ĵ 2U* e [mנye6[CNqګoSiDeغ\nnio=] &7`4JXj/ ЎB;=N QN[+hQB]yҍ(҉mV__@Xacy om΅v<Ŕj<By<ǘXKz cΥۼM1=ej|zΧPH7Ha%U(r.؎ aۚln qsqKă,) <{-(q&HYOpX~+F֧w)wO9c.,oܗn (u%F"n[>)I*wɅ-Y|ȥ% ѷ#%M7ŖɂK"wڬϑfPz=FU׈苲M)Kh*@B*.:K€ͩLϦ~Rc0X`an2Eaxe_-a!v9`O/T{Qd :Juxϼ~?qPϨ~?qPϨ~?q/@ž}>Q3߹o75}ϯ~׿q>~Ͽ~[>fϡoS~gٿs_?]3~ϼ?g3?~ϰ_w_/Y1/>U};Ͼq>gqϱs?WbO>SϽq>}>}~Ϫq?_}g3?}'ϳ+*HXA,_X>,h LïmAk2PK>q2bmhL* u&*déA%ecGrcȗ˅~f %"n9?q]e'.(7@Kwmt:@#Ӳ+c>YlCmZ5f>h ^^8KݼqC :W2ȔmIQ_fɚp՜'Q{.>5zrnD&rET0ۆw#~/P_3:]6m[ATFA#ok(IݻEejc@TSc@< sA*\X^[L־u>tȦCȝ%=L;noؗ(d}n4xJ@[#\`m*䈝 ?#` EHzuIk0 ^ٟȾW[AvE#h{\)l` -Tj> LGM+ml~N_lCbj I2v/KnT!B~YXP͒6!F3y_rݠ_Д*6;^bw){ { qj1@Lpʼ6~#ٵB4IS}3kT2HS]{ꋿm_ @孲-vǑR>Ё[=Kꘞ*-)j>XQYR1O)4{*BQk[oE C^i>t-EαOv/s$[Ԃ 3} ȉ{UxK6Xbm^iw<ϕǘLQ/󠚝J(q=Ihg`dqVYlXG*4rZe'y)`4A@"h2p ~!nJ,Qx(}F3mWu &̷xWkpb\@XhOjQY`{-g0 ټj,ۂ^Dd%ٴ`3*Y vNl91pP*s^ى; 0`#uȏ7*kS/|L0ĠU2*.P%$$P/].98`m~k]Wt5 >!.']DBrs L jXy5K Jۣ~Kq_+OѵCl#3`44 E{DD`q-`a"O;vMG ř| u'.CCo;~muuuȼ4jqkEZ}WXuáDsh렒P2 X`Tx`5c4OTP-3_Y;$R hⅾ=>*9|h_$W֡xl<u`/vRwhsޯ1AC=JL"30[<7BۜP@zDWIn#(=B1 :h9VﻹQ7TJj_={|yJq -j0!-ynE 5krYeC 6ETܷ(DBdŶ;W[NBv"O *l1m]7yժ9C'Q*#K+C76,MGE}2z@HI:%v5j*!1 ;YD0 Lv fJtBYZPP+tP1l2+-~҃!%×xӨa/\Iٵ\?2鷼Wg!lGIDL׍,LԵElCql ;3ۄv€T0l:Kй7=@Tc!p>PAß c{nK]nKUn P| E3JPdz;MG}bmcr^# y5z79Î/4IݽܔNz..-^ryjV.ZVѸ؃G]Yaw +L[o̲oJ.V܌4ªì|3mkGeqQ/pR+z0.e-x>V?RmR}wjQn5 *]@7w̵sUʄMq7^Qr8~eFstGB9pB0ӜԽKj$zUmʃ_2Mkeu{v?c`AZIDBP ZEd@cBmrɃM`ED>(%aNLwеu_vo'~hXuLƿ7./ 4a41jZ.ܠ* >r:SO]/֕i{{A- &mÆ*%iR=7yV>|@1C'J;z4>WCӨ[jrJ@D\c* N\[/+F6V fO0ϳ Xa'rj_O#AHgQ쓋BHf`ZῼM*J.$a*D.0fCZ CҀ^F6zJPA18QT5nr7s5cJ/P?g݁l=M#Lˆ_F0sc\@~JFAСr0Tig"/fŘn$F.:p.Pj0)ĆZ֞,HzCdx`6H{ޭ#ԶN82Xʎ$NL9G-ǀbYE¥B͍1h YpPv|v"yl93iG֠]=+Gun3C %@ j7A6wb(&F!b̀% X[ ʗXKP-w%hZzfu~Rji\M<1Y7xpPe˩]@PK -ф9gdOsT5ϕ{,Kg]`zo_LIP:MTTj˪Uq0ίxo;_Py/h*]l!ԲB}U0S׬/f @R6e 1 {J F)y:6jXkY ߚFP6'-@84Zv VubBu`E5L%S+k:\HUU\ӘYrogf {=cC?WBdQ- u<ͻy^iɷ}@8ZrL?0t/ Jzq:ڎ?"`DK7{MG;M_Ӌv7? *lG;DP VJ+JHZ I`AQt- @ᙶ-,l/t) KQl؟gq6Z>VK12WCp:v+]L*Q0Wu6a6v%Пiru1KveYN; %OYeVv6ۺ]:4 ĥl NLa W`Ь):1B)U8D]Z,Xy˥_Y1_2ZQkPrҋ8Uڠ6@'^AeG9-1QUl9\ԷAx?PZBԘNu9e*i  u] Fᘭ>'n,,0n@@ nm.̄c= oTtYZ78/ ChYA} V[TP6D2'YNE/@,{o:%UkB&cuS3|)V@&KP^[s2ڛ*K ܾS.|bǹjh;Pv{Cѿ HzòT1FG\=ō*B(ԪeeBQdGCP Bڛ}oZQṷүW-p}:[J_!er 8aߎ.>e+ǘo`O~пSo~O~ٿQ>߿O~ Poۭ\``~O~S럨{~O~ݿSߩ}>SJ% ]:)fC{[e4zʬ=+{NO~ӿPN'ڇsqs4? w}>fO~ٿQ7>fO~տSߩo}k>?{}{>VOQ_gO~ܿSオ>cOS-Bq22VxE`v12;Scmv|_W}>aOS?}>QG?o?a'S迩_o}鿨oKOS}>KG?s#?}O>sG}>OS~O_}>SO_}G~O~}#>@G}>YO~O}S>տ|E~}>ha2jVT< ?!ʅ )Uf".mbKah1dqZ?UPSqn^l ْ隖"F~Е+tB%i[y]9*M[5.E7Ύ_$[ : ˳ XP-yhPm@rxkwJFt9D=`H.?98;ٛc(;Tap`TSk5?%TJp &PtR UK㬯5PSszsmzBBeGѩ1looL@)lqJڠQش]4{mBį`mU&4\nb&Zt|d pR;|mP~`\^sTJgo̿LqMh(J*(\Oi`v0v* #Q4 Z=|7ijEz\mm=FZ,b5o:dkv-Ƒqixߡ-ΔU1EFj,M(]E],q5n*+E[%F-W-Eց|E IkBTs1aO/^,hR_8TA{(vvsέUֽ:;m70%KƿW0V?wQq*θ"GQMpz Jm 0V lŽ Qj(hТFZPL'iu -Ι}XL q Um~' +C7H]7ܥF.5 8T[3x* lcg5v, +"[DM5J> <):ԧ'+#gh +QiU`p;bW ?$魐=&QX>E[ v-_KaO`WO#H[|G(LLL-F)Kfx-¿nݗ-6%MmC\;x@v7ӥI͛Z:]{b*)KBxw&#*|qc2lG+Aib4QbSiNa SFn|jc{^[+=zBմa@?$P ;pY/h͉b 3K`d*\U bo .@U ;쀉Uh%r7{gmZsSUӧJB )pPrТdp<0jdeş+/H\ +8sDKM\} 6X/S$H.ZnC$7|i/e86窮2]T `}G?EA @Sf"Ωqe.,R4Ԥ Y TVZ^.~:ר۴y:Fnn]r8kF\-H9ދ}Ũs-Xmj/_@-ż[-@K-Ũ_\M[Ùu:1h[kEn-ЫA]}*pGm:2qnT S eeKFcIuW@[B7ȯPVYZ]]TrWU"x"C:5,p 26j9 ۻF^Ǚe5}r9r^bH4Tb {€.:q(Uzq,:-Ke$F Tԥmk uwO숪^Ү\Y [J7zSfB/K*Ϛjʍ?h!em- !y"E7ϋlmS(K{vp,_l'1你۸P.3 `&Pz&D_L)s6%+_"MZ @`K Qi\-ەr!PjY]&vgt̩FYⴠ4 9[jʍ&QaY[~gGYBjH;E"%QZn0Agzj ZfW[U#kfd[Tf[Δ^LMxܺ6z-3./iFQz-iFm6Իŭ*QkCЫBqn% QoZ"ދQn-G3mD q[QT o-qE㒢[Z-Ebs⸦eKmhA+cuv ҽ1GBb&J[Zplsn-E*m'S &}xvmWH. *eL1ˆ;ш@Mo$s*E+kg; a+nĶy!D7{ёu^9RL#ߜ[}sw""%ABr ,P0bm&1`@+lZn-fV PQ6Ngekun]ַ05j,nptoZQRب{ٛ؋0haAo, >\ X:h A Y̯P8VbzDnwEnx&k/ (3sNB]m*?O"/lQy^Y,Iٹ[`oP6jT.Dat*ޢ,cQK`9W1H:q15bŻ|Ke{nmFML^]`o1@/۬f2S+1X` savsF)Z0^F1emhqq K6"˘ CBTZ-zZw'j%iNbb[QEq.%˨ZEqj6\uf,=1n8ry]BQzQ[бj-EE-h(Y]UhkSk92hS x\ևS(9* f KeYf_ZĽ$s(mKj*e !wO"*`y=vwߴW*l-` pE'a9FZ9u^"ׇ^RrDqUu%)^"..˖IW7cq8\CYY[\n\Cs}eSe!lQvmG?iL{K:/H 0-гҁ4n6]a*HŤ-S{è6̫Eb˶-y ͞. ҳH %U6Gx$Ҏ/@AXzD۴OKNƝVվj(ZޔE~[Ѿr"ј0kE.MNQ6At)ҋHJE1n-ŸTh[;[~.EN]En-ŸQлmĺxً(83 JR(e}=cA ؀lKSK#i7e,,/r,#; M@,mDT(t9g0ÃaURj=a)7S՘E[{]1iiw H0,UH^JQk BC+pzՉ&p)́gE=( ~TnGAX"ty?d>9Wtl1b#}<B\=2Sa跣- [=gvt hFJ;Ǫ]E2Z2D+mFݽ۠_1rr-˭Dbx]w{-ǬޅW4lZqIuԪ.[ŋ]wQQqq*q([c.+B\%%S(a*ULZw˜`[-~'n_ƅܪjKq >)pyvG-+/}*,hć5VحUE[VSnU&.H􂴪; a淘Qc1*/\e@tQ) [eD)*p.pTv%%4sN rRUC/+23{nwe=C{V!=U GHk;3,*p6]2!7vS%(,Ф :Zؼ ײ+N:P/S[VXvxEΖtbKߍ qΩ|FX]\%6aQyJb_1j-gT] u3qk(n-E0QEZh\[n.[#[oEK j *j: 0i<31KcxZYķT`ne@h o=+hkf=88./Jvڏ2p۶ ZKHGX;uKXPf;Q+_\ _-H3uW IrTwX>pf Hv[iiVr.QBWdj*;q@(釿XFYVG 7ULsrf`")!/s5v `ܙ;Nت2$Mt-u{y7h/|478L b#&\T`8_L,C,|`֣~=QY݈<躍Dh "ަ#nmt.QMZ()u¶%(q\j(ފ-w6OBQj*bQG.j-htQaECq"TqqU;p2ylq+Up-1L3(9_*ZS_̤>X.V MίxsUm GsuP6>aWw 6!H,b8B!'hd+ _1 pQȞrZn(n Aa9X#G`dg;AanlTʩ*au5r)]ORv#m Fy11ha6@BY3Bz5y)o[)Աw?5KP RBVE ՘ _@6oPH۴)8'ncbrzA+FA'Ih5{X  FK#m%^Wo1>nƁ , סj-˩w, hs]]ݠ֗Z"%j n 1..qE^u>ZŨŨ;6@B_y[f-E%1+R]"J#T}#ER;QhW1EZ85z(ОAX: wMDP _Kaī.t3;2%+9*:/8]QX{1Y3wybN_ۆju==\FJD:U¶^“bz5.7cx@C(RviHte 1ʜԯO϶oޘVC!! M"Gr{-Y,WJ V-$;D^zB`YVSK7>YB2 e\kUUU-l%s1E U86,]wP"! VcEBao]!CiJÖ.4pA:r!SI~8DcQvsHы" X0 L5'i/BB54jɃt,˜-« ݺ!K)"-]eAjwy-Em[n-i\ƼG;qj.^t-0цna0۶jSwqZ_\QZwqvRŋRq^*Aٸ~"Z# QZtEQQ+.㲠$+\Gs=& @ (0"EP# Ym.#CoT\,0Rlhŷ7kUt[#=;B5cFY[U!Fyf,\ td h۷@9^8 . jnل5,Y^dٍ"" dq6 [B7!)-C-NE\/`29n(=vLYٮ)g,tprW ̠ruvܠ^BSa±tP]!WtX;Oi%Or; 0"08V~]@a3W06-}_{-w4S ^r\RIuWp/+x OE9!tXCsaDq0.)py| 0)nC"pprDmnpӒvAg<@KfXV3V1F;s.\X۴sċQzv WgפKyR1 ;꼧RPj .j)z"KE QnE-]q* fVًD[s-"QEQ]NJGn,q\[n8KY/:XVoO-Q rh-̪kWc2Y>q/9z$8Do ӼEP)Zt&Pp(An2kg;n NdhoIH '>[]D6-He0sȸ_cvGHpK U[eU`TfeL@yj$CU:x\R'Äd*axhW\&v^x얼B mN'A3Bʻܽ6,QAme]^n4EwԵUZ-L-%tm-n-ho@4Z9EqoRli-G"֊n E([URȀqn(0\Z[whQb8SQ9j-C{qދqeȩy;P\F:: (˧kI-{n} -OlmV0yw)]1,Wb][Ėq07bdy"Y+s2`z\af^<`V #vP,A#q_;u> |N8lVFeCNn`<@@DiΘ7rHv}̨DtbdM#Q* p[8m!b*,#jL&H.-QM0VKwcwcs@j\acVȪN?Ӣ+9 BH0?Kt5sNT”VLyH59cySh!tf?C;4DHl&doiq+ pUg*-o&5f˴:L vX[ J, zb!+QW`N.Q[H9 亙- m:mvV!uhAGlQyU(h>AǩEX [`"!ù\A9q Mz.:hxzN=Oϯt4a--e-xbح3ɱI[^} ~{&[ (gTИ!cԈbuP[oV3άy:J*PUA*KnkTUiu/.BפZO2 *a7 >BWa J4JR9S݂+;^2F.K0MH˹oAtv[ ”swdP?1)mw4&\kL =AXEZ]-R5V[{A 88w l6h]  2Wңv^ҙvlʱN"aa "4hPA(n/bFg CzmeAmJ`5kB`-N-eQ}`ވ%DTUvQbmٽ';X'ox 7 j6J&P]{] =(3{1oZ'J/\ )ʽ% JLAi٠fT.$3o*hߑ l6G'o%QaXWJ6I߼$}PG.XsNVQMIXN,(E7M5/%YU3-(_zڀ槸D>(Q^y";w|aS8EqvD&Ҩ {fX!xyh[Q/DĨߠf-G<Ÿ ь;n%ޤǨAkKKtZtXS lZRrq"%h·N-ŨFzlHv`QDf Ʒj R!jJmxD+WD<)̠s,*"Ȼz`R l@ T."=M3#RMX4 U"hl`4+, H8bP nds!kGtǔi"CmA2U(FA/B6jxn4pgJE&hX5Z`LV=ͥ [Za[,tcTVӭˌo^n{lAԍ%Nj i pjeքpEӳ|^pՊ` ۗD/Z޳Ds<(95:dKei΅e]XJQ.{( x3v,`rbNKJ5Vv8ܨXpq+h)$[%==7ՙ3Us j?C6ED1K׻;1auNJJwn"drob(vbް+`C]:ALv”9_U@9QVb!C0>*nG#Oah؆F(t *^f,P.U _1j9-0T;|gd ?VSr-s^N|]X? N=i@mT\VٽjİJ&['~ U }-E*#mɫoa556#` X%hKZf(;=&q 2 z!4TY"X"=Td i  ʾ~WL;\Qui0T-`-)aC:li N̚(DQj-訕]@|E]4ˊ`IiQ:hO^Sgh bP_GlĆtIk7B\GCGhp**KQ@ꚨC n1hqklwj.'Mp8fFT_a/ wo6l_E<4h9,}ƙv᪂bo50Qpm<[A*RUzU : ) DEC8*tS#CG`)"!5͌y{XPi 4vm1guXj8c8~n(h|Ŧ&\BT1U_ `f̰Bqi6Љ4f҉ZT%Jb ,N} c^ݗChc{\Ga &PG'`n|1Rr\9>4r" nסw`gE48[>#:_ЫS@@ka*VdpQ/^2~>na19MzV`[U:"a_FR6u!a]߭mh華KpZ R.lUdm $h‘(2ɜ,qXCb&/+3V7jj8!D쒇Po͘ bÑ8MDQ] YzqS*?_sVJyaut}F벷GsߤZnPd~6Qn-) d2jtHWrH ږY$pݭQ$V9ٛvô\͓ N+9Pr̴D#mxyjU%Yjj }`n=clp]+h bĸJ}& ,aMC n-ʿ X£eVJo-]D$Q@Feԏօ﫜*c2eZ"zoBnޚpgP_;aںqam|0.U PYW @oa=R1pg`g~ Bfhcvcm:-tC iyQZnP+.~ ,U kcFcW^{,uFU5~~InnMk@_#oXq 3+VU(HsᠣUBfF])0C{'5_\ JHWC2V҅Uw{aىzOY ὄfosDQ.%L"~Or݂ h+@ك0 ˲mT,~"-Ҋn&:JAx6!'nۈɾ5 Ea9@CN(8>Zݴ[T1.z|6>Zt&y‡Ɍ7GBxG1 [!vtH\]ԡ -KN.žJA= ijܑm93hRW`$Jk8=.ˮ(6JZS= -O3ګ~+"1% '"Y&U NY n٠v9)35u]xWP . `|@=㼽*qJ;7*$ ggႊ33Q"pНHL9 U6!NJ⿑#7dj\%rJ<mlHt}Ź{taSخ̎OT.*,䵟X6i†$CsPp`Fj\ުY e K߳ c{+KT|?`[}1;FK/E@+&3]er'#s!Ty+ZF$bz(:DCE;X+Ji Loe1e/zF uܱ q ds e>a{m'p J x|0)F)W9MgZ~б[#|5qB5W\3Ḙۨu)E_y&Q)O˪5sp @Pʾh%_dM%#h a!جk)בŵG\x@(!{zZDW%9UlWqE_[ 7, Uv\XZGQ14g%ynnF4X)cGD]' ԁBp[P0[aH\Tt9m50ZbrܩgrXwipc FDGr nBQb^mC?ߠ3ꔁ$bHUɱK [ ﲉxeN ڢr-C,gQA,O&XC$!y̚w6GԳu8K:A:4 qj î\"uMT&ܱ15*x/!|U`Z*p)wrBlfj sb~%]iì"+NO&%0{_O{\#`L`%-Ao-jYtcPThꯖyhU^UPHR&DSL6ijgvۗh}:p [>@@-Piq/l]l`BBjdl@$lE-h*%k.F~ɇ)NZBP D; )^r2C[851twR&Ө2'p[Ň elY+|/`%1Y\!@\۔ݲV>SKkJd 3:rE,̈́jW cQP~->..X {&prUdAwȍD"Yj+!7kt6t`֖;Jg(X9&V,itc@'lvoGOm4*[Q;EuSW0Viku_,ITML]<gkxܡQ!&0 Yef&\.9.]  -<m`ҭV(E՝DW_lHL+5[f3<Z9wHZIv)@9DтQb<mmt/k @D7kR{ Iʰ: ޻NZu %FJ(TB  뢜4ݠaj|,Un<3'tIHnRYc[hY"1Zb{Jϰ(Ggha%ϴB5Al{DNPhE Ÿ_ pӷIgeQD[2% p^ U>W2][׋KfT VN±8t6ۏ-hбnP x|6 R!f5zfkѳ -q$aPnL U`Ah%X윎ahC0(Ō>kĭ ,) Ua+:mQ.4'0/[Ve:-F&\C(b_azWY2, B}`%U=@n"~PǍCz]EJ3 პ{m&E}Nous7]h *<ښ,lbU q Yxo:p3c [ؕ"e@,ul.Ke)%=ifQn-h+qK}젱V-Ozki;I*dFiJ3QP rA?m࿉+ p63^eԭ7I  J1#wJL7zlR1ݟ:YeLM=Guܦ&cd^߄#Y`ڏ>@ؼl(U5QˬN F-"u6pˠT.J6X(J@,j mEsm8EߘIigs,-*AJTZO ~=f#WyoKh шA)/fJ*G#S۔^K#g5#g 6Uq_蚢Qra)VK/ G#Sn^$`kCu \c)`'PE_xGArΠ%e1Wr6"?1jgeV#mwSq </3ffR'r7 sPqir#{헣XZ\[t-8z*Ӓ(1`=m,>|?ǀiRP"_i@_<؅"8J\tqbu䴣L8}8pd!&c,p^ז %c`\y]dǟWO5 [" 8f h 443;/o܆{L@$xWk։(_ES6  MO$>?۷XZnԩR.TB&U Ih(GPaR`Ia ZMX)n=:j\pXj%>:F-{^KZjקRhXB֭`a%mwƒɌ^Q} KIC5`"2,.$3Bz'R͏Q2r̔6t$+bvgϩzgr]? ͏B ukEKդZ+mZ 1qiS/zvDP>[.z,V ՠ7n4K* 7m )bw<EcCemsv;@ͶENK;!yĪ4$y%FvE.MJՔ޻J)Z6(`cPF)iq sl UūxsWR9q2PYu@kZy Krw kaS޾rEQqa`s.0=PNb|mL=j (yQ{nxa_pYY9h (9B^aBiM` z"!4ZNRKA5i/soI)lۥ0^")l"H$e^E0h .BE,IDfQ̻q__/ůqi}}qOq)L>O-~⟧-}?q_o q-+⏧[}?~߸徟_}?qB2(_8ˎhh?Qo}Qg:RGvXYV}6ѐ̋d`#@xb-%@S ab @c1$~i!d&{csoP`7"ph@ EZ!~J  T}. C$Gz ]@I'Wb;v$ f%G` bf1cKǀB/5#`,;&]G]g~mtLv''V-evkF/}>g(Wb1g"]YqK5]XɆ.:'6|0 ZV@AB3ءq?bOσޘN>4ցt?戽.ß?Y0 nA,)Q񁼖2T~^M3hwx*U V¸pN9` >Ü$eDwL\Uic*2tTd@hCf>?~И횺\GgvqȒ2s͛)D6$ 1MU{s(7]Z>C"@Ku7  $=v ͣ)\RcyԾ=a"|*|G6t]w.,M+-#ɣв@[|h< t_H+ |zLR,_J/4v90e7 vD-@(~B|p~ïYTUl ;!@jb?Lg~ @n9h aaTmGdw4+Ys4*l,IN[&+vޚP,P,b;Up1BDǴZA2 pz cfߔw >Q[M =n+#t7 KIY9U;MyE+-6 %)p~0e-]ֶȷH/4qEqSB(j]T 17-PنE^2-@ߥ; m( DݻSqmx*2 +E -3trHes"Os і:uu r`FPR.w2,Aj73KU*%э-M0E #PYџ+^}EjXE+ǢvGK=2 ɨ%΋}0X/ 1h<C̢_xW<|z"H0 9(]bT-D{d`fĪ SJ!thѿlio݃X3Kr-.i`hbMP*n!Cdk?PP@VV[O{O0U m,͆\ Fhb4G Qw&eEn߻eZ.b;l 0̆vd2Tł]LN (IñW1.@W Ya>Q;VǞ!gϔIU:n'#]+d[ l} M@Db/RN0@+hlJG$`齬tq8_e/A3{rI 2BZPgpt: axݑeWjNY|b=^4"/ѫmAu.UKJe_RQQ}(YF,}@]_@/bǭEۼX,:ǢΊl$ 6/7=&w;Jmh 0LE}r4!@KZASl(VޟJ Eb-}Կd}'u]M$dW0_m 3#ƕ}7}f(bHBXdbgtYp/A º/p/Ukb{' K#䕻m3 qtÔ `th\gi& h} (`)6 ۴s*WmB|*[KK5Vh.1qŋQh2uy }!GF_AtLx_@TI?f?>^?3qZPL aƉRK-#8,d_gx%F-bSv'e}{  ^y-.xUB\Xyeh+4 @m]x D;v$vČߡF~dUf 6aKUi}B 4Z/cJ璗W鍴pEhM~#L d Cڃ_mHTwB.BA17.?2'X4&;rI7~ >Og=޿ X4m dmP]vIi:A > kYVj?p{*j,]_}Ld_CyKǟ@|*.X B<}E8m`܆ /}@mO&SV+uũT<;9ps~b>e`{3,PFmTv>йqƐ#dv̖` hOi?lBo eQ^!a@Q},:>-bU,Yк,]z>AD)dC/Y(:Ex::Kǟ\2/=}cΡERX@+}}޼PïMmD8DH–2({t"mpB U~CIf 01˴>ɭL G21d j}7(0=8 OĆj.ѣ]<Bv}_-d,+5::viH!,,eҗGǮEDE+E,Xtbŏ,|iDXagǪY}`G@.Ǚt|*,yX%Zцsmdvex4n3b.CaTlq kqPaK !3 ߒpv5?$sSb}#H(>"2Q틹rGN CY~/2~3+Sk޷ͱo-uA-@S!AyD yԞt@4+Y!Za::hDKn4tE:>GgN ]1e"y|E԰}h<]C<,|h]b} բn 7Ɠ΁ϣg @m |3J}bיtNw~xPM ~2vAs@Ojf 4Ku|C1a a0RUc@ ^V@VqL^M@4M*>:)mtXܕu%,C$pkCO&ves߄Kn*V{vNϧT .Z-],J=µXhR^uQJQĢEi}}/}Z_xE_y},_Li}<ǝf'ӧ{> 1'"?3G*ߠߐ'ܕb*dm)+7, C;@e-P'w]E·xH)ۀFb7F(?f?B C/hVQӳm͞TkC2XA8|_f۬ ,!oR{ P;@.``CTZJZJ QkKt,7kV pMl $N{tz'^u´Y9% +PHءn`^/*w#dmNpn6Lк=Lt~޷FYyо]K>{>},#Y/]IWJ6/pyJ!O)Om\'TOh߁?WfL^ 1\ɃG;6/}?3z -w` r[`VdO"W.3/ꁉlx1l'%ͻ#q-GlB-m.R=&CU&X&(2jo4V0FYKu8AV%<f‘VTXct*$9.8ڛCl`0jTmbKe.& wl}{9ό3Xӷm9,ԝJ!25oܛ˜`Gwtz}+tۼϕꞎE]5=:/@yyG!fCΕ|FG<1CqΫjCz0KIKNK5}ĉTC1Q8iԁv _ku- =(5{U;͛^^w%!A ~ʮmA(bd&oM]̄xĊ@\WG, CpܧKq*<}Xۺʰi45+ -chg5sAgnK: aG-%1Kdk)ܵ<'~P: Ч>1?xN&l:1)K&* տmh%nfcQ .[3p_6O ) %ݲz/nVNo֍OB WWCy, FB΄YGX:}C4H<|E ZG%W/<<ŎA/rk]>Wvк:>4yϣv=;v /0|`3CFAquV` \o +%Qbn %VmWǰ'`v7ALWl7+KSe[ HQp7g$*RTKvab~,{>˼<U7J 2,11\<6n&fr>;8aE *ݩ`sUh)g"<,` 6e-S /G/ \y%qmvIHVn|)‚0 i~M=w9̝ x݇ k i/Ҽ`BkX%pa 44wi[އH/ "TrF6THP,-z}:(Ӛ+#UO]ϗ/ݥ՗h,eϠ<|zAGKjYuzա<.ݩuy}]GA}'ƧIfP񭞠gyǘ/=Z^GRb, o,靓;>8j`'Dc!c&e"#2r. @a^Zs1l1PUyg Sݎ\mڛTFo`/S/n$H$E"ٚ7]rjVRtH[BU5{4sA1 Q`2uG3b ܫۼ''>e Cf;a)Z1ۘL:16- /KF/EVд"k|6'^̲ς#,O Q/%g(zsRۄv,uv ]~p/[LǧLv[,xG=<1GK2BM_S:]z$PFp}cR/] {h$zCE}_y}ŋ.Ϡ:<_Xyj,y_P1tL >ǣG|/ lAv Ŝ Y` Y x` +b/ybt`\'y-tEڡ%ty~qhG@7EEܟqaX<9+tbxW)]pTPL~D{e$.u1!D.80'j(Ώ:2/ΣQfǠjEj\оM,EIkK>p"]]G./sIR-Aw`6Gˊ %RXbΚ|* F5piUPԂ V*ArúÖ?=.V>9X>,h Lî K2hZ_x*cl7Y (qJY3:oR0Qe6nb"<3sꜻm nh@X#D&(t=n;Mw(6"G,,cM"硓hza `LuY h; ;6d|K|J Sh7+^Oydyo$iĶW݅U;yUBΖ .#`Q 9#(uxx<]ֳ`d]lH[0XD^f5&KS6~aw|UFmh*ΠFnd7DAT&ALSfozABb f{A쥂*(ҿ-~8ΡMg%)Q# Ut$M ǙJڼzbQ:#S@T8sFd?>7ɺHk9y VV=į~Dn6Arkh&shm%ߵ50QuslEVbQtevdSU px AX#fy(  ?L2^仜1면.ekCB?_Iy!v*^ko:/xO Agh^SkXD#p 7`a[w`?)"@'t?`O}(*>jD[( Сo]c[;[Df`\ty_Y. ^(c7]6%.\ ɠ ț)m6mW6j fq,,.Xu&rceKp9tz cj6E_Ǣ f$@Z!7  ZSK] fɐtf-:n&͗Cw؃=68]pfӑ0gDȐ".jq]w` ZM9Rڱ7}<"Dz$RIi`UyX83(Un.߳A%pVS %woW9WR?*$oI&-`ٕi 1(lM-GT|uP|'tzgU1Ź=pf!?LvHNH*ߕY|@b%rC B$P׶l/g0B.W:5b/5 )߱ ={UbGWW4&·/r6 @;^U ?um1^MK3 q2GUp_}N}CN,:ܝ/(b\'0Xxx|Uswvn%2WE%n ax~SW^]W?qwO__:dlUў8ؗ;Q.-h)j% 2[((A{p-loNݫŘkOm4ܲ\06Ɉc`KD;>н8y]g!3saC4!ܭ/Kw+}|e,dSJ!ߚE1ҍp/629{WiWU- qH"ܰ˘'Uu,-yq3UxbEW*;<ǝSK]*sn6nZ8}i(b`4@8Uij#z#Czk =1=:@zM" g@f^Be5YMH4 t('F(tN{c9=qJ*%ox;C.)bgQJ^`f! H@=vۨ\7#Y9A;R h{ǯ3ߌ=f:1"oťaP:a=Hq_̳{?LVeZl@/R(l`v ՑAEus`.u~B嶓lp3K&ig~]-3D&YJVhKVCfa5oax!9e: .[FMCWxJu D0x悀>n-bl7`˾;?̾tB_F2ACz@!f +;.Oyb(+CeƬ:l`{˘B#-ℳ ] {l -]At_} )R1^F o}T6=ajd6T-q{rê]F.J|2Fm:KblU=Ғ ]Hh@pEFچӢ"r<)$i=1b՚$̰^bN)N.`7bš-Fr$R"p7j @42q[ vK=0^Ӥe%̼ DKK7c-CmັfGyd'!nj|%E\JJeQ>AcӠ'j3c Ǖ" ]? AkzplĊݮ rg2pݫ4bl]i-Dp lQ9U8[QnQT9Ģ66xhFFVS!!A[bʋnȎD+|6:'DUSWBKJ)J 7tk yc5ϤXLs| s tZ6hD^p .P8j+t ֈ?+z} h:zO׶xzczcjx5<%UheT%@WSO6fS :=f&úLYigJeI~^ /KJoP 0£ :Ŵ[ 00hmU_=)/(Ka @?dangvd-crystal-dock-7a34e96/src/000077500000000000000000000000001512233520000164765ustar00rootroot00000000000000dangvd-crystal-dock-7a34e96/src/CMakeLists.txt000066400000000000000000000123461512233520000212440ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.20) project(crystal-dock) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) # When doing code clean-up, uncomment this. # add_compile_options(-Wall -Werror) # When doing code clean-up, comment this. add_definitions(-DQT_NO_DEPRECATED_WARNINGS) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) find_package(Qt6 6.6 REQUIRED COMPONENTS DBus Gui Test Widgets) if (Qt6_VERSION VERSION_GREATER_EQUAL 6.9.0) set(QT_NO_PRIVATE_MODULE_WARNING ON) find_package(Qt6 REQUIRED COMPONENTS GuiPrivate) endif() find_package(Wayland 1.22 REQUIRED COMPONENTS Client) find_package(LayerShellQt 6.0 REQUIRED) set(SRCS desktop/desktop_env.cc desktop/budgie_desktop_env.cc desktop/hyprland_desktop_env.cc desktop/kde_desktop_env.cc desktop/labwc_desktop_env.cc desktop/lxqt_desktop_env.cc desktop/niri_desktop_env.cc desktop/sway_desktop_env.cc desktop/wayfire_desktop_env.cc display/window_system.cc display/kde_auto_hide_manager.cc display/kde_virtual_desktop_manager.cc display/kde_window_manager.cc display/kde_screen_edge.c display/plasma_virtual_desktop.c display/plasma_window_management.c display/wlr_foreign_toplevel_management.c display/wlr_window_manager.cc model/application_menu_config.cc model/config_helper.cc model/launcher_config.cc model/multi_dock_model.cc view/add_panel_dialog.cc view/appearance_settings_dialog.cc view/application_menu_settings_dialog.cc view/application_menu.cc view/battery_indicator.cc view/calendar.cc view/clock.cc view/color_button.cc view/desktop_selector.cc view/dock_item.cc view/dock_panel.cc view/edit_keyboard_layouts_dialog.cc view/edit_launchers_dialog.cc view/icon_based_dock_item.cc view/icon_button.cc view/iconless_dock_item.cc view/keyboard_layout.cc view/multi_dock_view.cc view/program.cc view/separator.cc view/trash.cc view/version_checker.cc view/volume_control.cc view/task_manager_settings_dialog.cc view/wallpaper_settings_dialog.cc view/wifi_connection_dialog.cc view/wifi_manager.cc utils/desktop_file.cc desktop/desktop_env.h desktop/budgie_desktop_env.h desktop/hyprland_desktop_env.h desktop/kde_desktop_env.h desktop/labwc_desktop_env.h desktop/lxqt_desktop_env.h desktop/niri_desktop_env.h desktop/sway_desktop_env.h desktop/wayfire_desktop_env.h display/window_system.h display/kde_auto_hide_manager.h display/kde_virtual_desktop_manager.h display/kde_window_manager.h display/kde_screen_edge.h display/plasma_virtual_desktop.h display/plasma_window_management.h display/wlr_foreign_toplevel_management.h display/wlr_window_manager.h model/application_menu_config.h model/application_menu_entry.h model/config_helper.h model/launcher_config.h model/multi_dock_model.h view/add_panel_dialog.h view/appearance_settings_dialog.h view/application_menu_settings_dialog.h view/application_menu.h view/battery_indicator.h view/calendar.h view/clock.h view/color_button.h view/desktop_selector.h view/dock_item.h view/dock_panel.h view/edit_keyboard_layouts_dialog.h view/edit_launchers_dialog.h view/icon_based_dock_item.h view/icon_button.h view/iconless_dock_item.h view/keyboard_layout.h view/multi_dock_view.h view/program.h view/separator.h view/trash.h view/version_checker.h view/volume_control.h view/task_manager_settings_dialog.h view/wallpaper_settings_dialog.h view/wifi_connection_dialog.h view/wifi_manager.h utils/command_utils.h utils/desktop_file.h utils/draw_utils.h utils/font_utils.h utils/icon_utils.h utils/math_utils.h utils/menu_utils.h view/add_panel_dialog.ui view/appearance_settings_dialog.ui view/application_menu_settings_dialog.ui view/edit_keyboard_layouts_dialog.ui view/edit_launchers_dialog.ui view/task_manager_settings_dialog.ui view/wallpaper_settings_dialog.ui view/wifi_connection_dialog.ui) add_library(crystal-dock_lib STATIC ${SRCS}) set(LIBS Qt6::DBus Qt6::GuiPrivate Qt6::Widgets Wayland::Client LayerShellQt::Interface) target_link_libraries(crystal-dock_lib ${LIBS}) add_executable(crystal-dock main.cc) target_link_libraries(crystal-dock crystal-dock_lib ${LIBS}) configure_file(crystal-dock.desktop.cmake ${CMAKE_CURRENT_BINARY_DIR}/crystal-dock.desktop @ONLY) # Install install(TARGETS crystal-dock RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/crystal-dock.desktop DESTINATION ${CMAKE_INSTALL_PREFIX}/share/applications) # Uninstall configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" @ONLY) add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) # Test enable_testing() add_executable(application_menu_config_test model/application_menu_config_test.cc) target_link_libraries(application_menu_config_test Qt6::Test crystal-dock_lib ${LIBS}) add_test(application_menu_config_test application_menu_config_test) dangvd-crystal-dock-7a34e96/src/cmake_uninstall.cmake.in000066400000000000000000000020201512233520000232500ustar00rootroot00000000000000if(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") message(FATAL_ERROR "Cannot find install manifest: @CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") endif(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files) string(REGEX REPLACE "\n" ";" files "${files}") foreach(file ${files}) message(STATUS "Uninstalling $ENV{DESTDIR}${file}") if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") execute_process( COMMAND "@CMAKE_COMMAND@" -E rm -f "$ENV{DESTDIR}${file}" OUTPUT_VARIABLE rm_out RESULT_VARIABLE rm_retval ) if(NOT "${rm_retval}" STREQUAL 0) message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}") endif(NOT "${rm_retval}" STREQUAL 0) else(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") message(STATUS "File $ENV{DESTDIR}${file} does not exist.") endif(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") endforeach(file) dangvd-crystal-dock-7a34e96/src/crystal-dock.desktop.cmake000066400000000000000000000004271512233520000235520ustar00rootroot00000000000000[Desktop Entry] Type=Application Name=Crystal Dock GenericName=Desktop Panel Icon=user-desktop Exec=@CMAKE_INSTALL_PREFIX@/bin/crystal-dock Terminal=false Categories=Utility; Keywords=Dock;Launcher;Taskbar;Desktop;Panel; X-KDE-Wayland-Interfaces=org_kde_plasma_window_management dangvd-crystal-dock-7a34e96/src/desktop/000077500000000000000000000000001512233520000201475ustar00rootroot00000000000000dangvd-crystal-dock-7a34e96/src/desktop/budgie_desktop_env.cc000066400000000000000000000033511512233520000243200ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "budgie_desktop_env.h" #include namespace crystaldock { std::vector BudgieDesktopEnv::getApplicationMenuSystemCategories() const { static const std::vector kSystemCategories = { {"Session", "Session", "system-log-out", { {kLockScreenId, "Lock Screen", "", "system-lock-screen", "dbus-send --type=method_call --dest=org.buddiesofbudgie.BudgieScreenlock" " /org/buddiesofbudgie/Screenlock org.buddiesofbudgie.BudgieScreenlock.Lock", "" }, {kLogOutId, "Log Out", "", "system-log-out", "budgie-session-quit", "" }, }, }, }; return kSystemCategories; } std::vector BudgieDesktopEnv::getDefaultLaunchers() const { return { kShowDesktopId, defaultWebBrowser(), kSeparatorId, "budgie-desktop-settings", kLockScreenId, kLogOutId, kSeparatorId }; } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/desktop/budgie_desktop_env.h000066400000000000000000000022031512233520000241550ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTAL_DOCK_BUDGIE_DESKTOP_ENV_H_ #define CRYSTAL_DOCK_BUDGIE_DESKTOP_ENV_H_ #include "desktop_env.h" namespace crystaldock { class BudgieDesktopEnv : public DesktopEnv { public: std::vector getApplicationMenuSystemCategories() const override; std::vector getDefaultLaunchers() const override; }; } // namespace crystaldock #endif // CRYSTAL_DOCK_BUDGIE_DESKTOP_ENV_H_ dangvd-crystal-dock-7a34e96/src/desktop/desktop_env.cc000066400000000000000000000065111512233520000230020ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "desktop_env.h" #include #include #include #include #include "budgie_desktop_env.h" #include "hyprland_desktop_env.h" #include "kde_desktop_env.h" #include "labwc_desktop_env.h" #include "lxqt_desktop_env.h" #include "niri_desktop_env.h" #include "sway_desktop_env.h" #include "wayfire_desktop_env.h" #include #include namespace crystaldock { DesktopEnv* DesktopEnv::getDesktopEnv() { QString currentDesktopEnv = getDesktopEnvName(); if (currentDesktopEnv == "Budgie") { static std::unique_ptr budgie(new BudgieDesktopEnv); return budgie.get(); } else if (currentDesktopEnv == "Hyprland") { static std::unique_ptr hyprland(new HyprlandDesktopEnv); return hyprland.get(); } else if (currentDesktopEnv == "KDE") { static std::unique_ptr kde(new KdeDesktopEnv); return kde.get(); } else if (currentDesktopEnv == "labwc") { static std::unique_ptr labwc(new LabwcDesktopEnv); return labwc.get(); } else if (currentDesktopEnv == "LXQt") { static std::unique_ptr lxqt(new LxqtDesktopEnv); return lxqt.get(); } else if (currentDesktopEnv == "niri") { static std::unique_ptr niri(new NiriDesktopEnv); return niri.get(); } else if (currentDesktopEnv == "sway") { static std::unique_ptr sway(new SwayDesktopEnv); return sway.get(); } else if (currentDesktopEnv == "Wayfire") { static std::unique_ptr wayfire(new WayfireDesktopEnv); return wayfire.get(); } static std::unique_ptr generic(new DesktopEnv); return generic.get(); } QString DesktopEnv::getDesktopEnvName() { QStringList desktops = qEnvironmentVariable("XDG_CURRENT_DESKTOP").split(",", Qt::SkipEmptyParts); // The format is usually something like 'KDE' or 'labwc:wlroots'. constexpr char kGenericDesktop[] = "generic"; return desktops.isEmpty() ? kGenericDesktop : desktops.first().mid(0, desktops.first().indexOf(':')); } std::vector DesktopEnv::getDefaultLaunchers() const { return { defaultWebBrowser() }; } QString DesktopEnv::defaultWebBrowser() const { QProcess process; process.start("xdg-settings", {"get", "default-web-browser"}); process.waitForFinished(1000 /*msecs*/); QString desktopFile = process.readAllStandardOutput().trimmed(); auto appId = desktopFile.first(desktopFile.lastIndexOf('.')); return !appId.isEmpty() ? appId : "firefox"; } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/desktop/desktop_env.h000066400000000000000000000052101512233520000226370ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_DESKTOP_ENV_H_ #define CRYSTALDOCK_DESKTOP_ENV_H_ #include #include #include namespace crystaldock { // Desktop Environment (including lightweight compositor-only environments) specific data/logic. // Currently supports Hyprland, KDE, Labwc, LXQt, Niri and Wayfire. class DesktopEnv { protected: DesktopEnv() = default; public: virtual ~DesktopEnv() = default; static DesktopEnv* getDesktopEnv(); static QString getDesktopEnvName(); // Return true for tiling Wayland compositors (e.g. Hyprland, Niri). virtual bool isTiling() const { return false; } virtual QString getApplicationMenuIcon() const { return "start-here"; } // System categories (e.g. Session/Power) on the Application Menu. virtual std::vector getApplicationMenuSystemCategories() const { return {}; } // Default launchers. // e.g. File Manager, Console, System Settings. // Returns a list of app IDs. virtual std::vector getDefaultLaunchers() const; // Does the DE support setting wallpaper programmatically? virtual bool canSetWallpaper() const { return false; } // Supports separate wallpapers for separate screens. virtual bool supportSeparateSreenWallpapers() const { return false; } // Sets the wallpaper for the current desktop for the specified screen. // If the desktop environment does not support separate wallpapers for // separate screens, this simply sets the wallpaper for the current desktop // for all screens. // Args: // screen: screen to set wallpaper for. // wallpaper: path to the wallpaper file. virtual bool setWallpaper(int screen, const QString& wallpaper) { return false; } // Returns the app ID of the default web browser. // Uses Firefox as fallback if default web browser not set. QString defaultWebBrowser() const; }; } // namespace crystaldock #endif // CRYSTALDOCK_DESKTOP_ENV_H_ dangvd-crystal-dock-7a34e96/src/desktop/hyprland_desktop_env.cc000066400000000000000000000030641512233520000247030ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "hyprland_desktop_env.h" #include namespace crystaldock { std::vector HyprlandDesktopEnv::getApplicationMenuSystemCategories() const { static const std::vector kSystemCategories = { {"Session", "Session", "system-log-out", { {kLockScreenId, "Lock Screen", "", "system-lock-screen", "hyprlock", "" }, {kLogOutId, "Log Out", "", "system-log-out", "hyprctl dispatch exit", "" }, }, }, }; return kSystemCategories; } std::vector HyprlandDesktopEnv::getDefaultLaunchers() const { return { defaultWebBrowser(), "kitty", kSeparatorId, kLockScreenId, kLogOutId, kSeparatorId }; } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/desktop/hyprland_desktop_env.h000066400000000000000000000022271512233520000245450ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef HYPRLAND_DESKTOP_ENV_H_ #define HYPRLAND_DESKTOP_ENV_H_ #include "desktop_env.h" namespace crystaldock { class HyprlandDesktopEnv : public DesktopEnv { public: bool isTiling() const override { return true; } std::vector getApplicationMenuSystemCategories() const override; std::vector getDefaultLaunchers() const override; }; } // namespace crystaldock #endif // HYPRLAND_DESKTOP_ENV_H_ dangvd-crystal-dock-7a34e96/src/desktop/kde_desktop_env.cc000066400000000000000000000056621512233520000236330ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "kde_desktop_env.h" #include #include #include #include #include namespace crystaldock { KdeDesktopEnv::KdeDesktopEnv() : plasmaShellDBus_("org.kde.plasmashell", "/PlasmaShell", "org.kde.PlasmaShell") { qdbusCommand_ = commandExists({"qdbus6", "qdbus-qt6", "qdbus"}); if (qdbusCommand_.size() == 0) { std::cerr << "Could not find QDBus command. Certain functionalities will not work." << std::endl; } } std::vector KdeDesktopEnv::getApplicationMenuSystemCategories() const { static const std::vector kSystemCategories = { {"Session", "Session", "system-log-out", { {kLockScreenId, "Lock Screen", "", "system-lock-screen", "xdg-screensaver lock", ""}, {kLogOutId, "Log Out", "", "system-log-out", qdbusCommand_ + " org.kde.LogoutPrompt /LogoutPrompt promptLogout", ""}, } }, {"Power", "Power", "system-shutdown", { {"reboot", "Reboot", "", "system-reboot", qdbusCommand_ + " org.kde.LogoutPrompt /LogoutPrompt promptReboot", ""}, {"shutdown", "Shut Down", "", "system-shutdown", qdbusCommand_ + " org.kde.LogoutPrompt /LogoutPrompt promptShutDown", ""} } } }; return kSystemCategories; } std::vector KdeDesktopEnv::getDefaultLaunchers() const { return { kShowDesktopId, defaultWebBrowser(), "org.kde.konsole", "org.kde.dolphin", kSeparatorId, "systemsettings", kLockScreenId, kLogOutId, kSeparatorId }; } bool KdeDesktopEnv::setWallpaper(int screen, const QString& wallpaper) { const QDBusMessage response = plasmaShellDBus_.call( "evaluateScript", "var allDesktops = desktops();" "d = allDesktops[" + QString::number(screen) + "];" + "d.wallpaperPlugin ='org.kde.image';" "d.currentConfigGroup = Array('Wallpaper', 'org.kde.image','General');" "d.writeConfig('Image','file://" + wallpaper + "')"); return response.type() != QDBusMessage::ErrorMessage; } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/desktop/kde_desktop_env.h000066400000000000000000000030021512233520000234570ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_KDE_DESKTOP_ENV_H_ #define CRYSTALDOCK_KDE_DESKTOP_ENV_H_ #include "desktop_env.h" #include namespace crystaldock { class KdeDesktopEnv : public DesktopEnv { public: KdeDesktopEnv(); QString getApplicationMenuIcon() const override { return "start-here-kde"; } std::vector getApplicationMenuSystemCategories() const override; std::vector getDefaultLaunchers() const override; bool canSetWallpaper() const override { return true; } bool supportSeparateSreenWallpapers() const override { return true; } bool setWallpaper(int screen, const QString& wallpaper) override; private: QDBusInterface plasmaShellDBus_; QString qdbusCommand_; }; } // namespace crystaldock #endif // CRYSTALDOCK_KDE_DESKTOP_ENV_H_ dangvd-crystal-dock-7a34e96/src/desktop/labwc_desktop_env.cc000066400000000000000000000030661512233520000241540ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "labwc_desktop_env.h" #include namespace crystaldock { std::vector LabwcDesktopEnv::getApplicationMenuSystemCategories() const { static const std::vector kSystemCategories = { {"Session", "Session", "system-log-out", { {kLockScreenId, "Lock Screen", "", "system-lock-screen", "swaylock", "" }, {kLogOutId, "Log Out", "", "system-log-out", "labwc --exit", "" }, }, }, }; return kSystemCategories; } std::vector LabwcDesktopEnv::getDefaultLaunchers() const { return { kShowDesktopId, defaultWebBrowser(), "alacritty", kSeparatorId, kLockScreenId, kLogOutId, kSeparatorId }; } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/desktop/labwc_desktop_env.h000066400000000000000000000021301512233520000240050ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef LABWC_DESKTOP_ENV_H_ #define LABWC_DESKTOP_ENV_H_ #include "desktop_env.h" namespace crystaldock { class LabwcDesktopEnv : public DesktopEnv { public: std::vector getApplicationMenuSystemCategories() const override; std::vector getDefaultLaunchers() const override; }; } // namespace crystaldock #endif // LABWC_DESKTOP_ENV_H_ dangvd-crystal-dock-7a34e96/src/desktop/lxqt_desktop_env.cc000066400000000000000000000041411512233520000240470ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "lxqt_desktop_env.h" #include #include namespace crystaldock { std::vector LxqtDesktopEnv::getApplicationMenuSystemCategories() const { static const std::vector kSystemCategories = { {"Session", "Session", "system-log-out", { {kLockScreenId, "Lock Screen", "", "system-lock-screen", "lxqt-leave --lockscreen", ""}, {kLogOutId, "Log Out", "", "system-log-out", "lxqt-leave --logout", ""}, } }, {"Power", "Power", "system-shutdown", { {"reboot", "Reboot", "", "system-reboot", "lxqt-leave --reboot", ""}, {"shutdown", "Shut Down", "", "system-shutdown", "lxqt-leave --shutdown", ""} } } }; return kSystemCategories; } std::vector LxqtDesktopEnv::getDefaultLaunchers() const { return { kShowDesktopId, defaultWebBrowser(), "qterminal", "pcmanfm-qt", kSeparatorId, "lxqt-config", kLockScreenId, kLogOutId, kSeparatorId }; } bool LxqtDesktopEnv::setWallpaper(int screen, const QString& wallpaper) { // LXQt doesn't support setting different wallpapers for different screens. return QProcess::startDetached("pcmanfm-qt", {"--set-wallpaper=" + wallpaper}); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/desktop/lxqt_desktop_env.h000066400000000000000000000023701512233520000237130ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_LXQT_DESKTOP_ENV_H_ #define CRYSTALDOCK_LXQT_DESKTOP_ENV_H_ #include "desktop_env.h" namespace crystaldock { class LxqtDesktopEnv : public DesktopEnv { public: std::vector getApplicationMenuSystemCategories() const override; std::vector getDefaultLaunchers() const override; bool canSetWallpaper() const override { return true; } bool setWallpaper(int screen, const QString& wallpaper) override; }; } // namespace crystaldock #endif // CRYSTALDOCK_LXQT_DESKTOP_ENV_H_ dangvd-crystal-dock-7a34e96/src/desktop/niri_desktop_env.cc000066400000000000000000000030531512233520000240210ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "niri_desktop_env.h" #include namespace crystaldock { std::vector NiriDesktopEnv::getApplicationMenuSystemCategories() const { static const std::vector kSystemCategories = { {"Session", "Session", "system-log-out", { {kLockScreenId, "Lock Screen", "", "system-lock-screen", "swaylock", "" }, {kLogOutId, "Log Out", "", "system-log-out", "niri msg action quit", "" }, }, }, }; return kSystemCategories; } std::vector NiriDesktopEnv::getDefaultLaunchers() const { return { defaultWebBrowser(), "alacritty", kSeparatorId, kLockScreenId, kLogOutId, kSeparatorId }; } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/desktop/niri_desktop_env.h000066400000000000000000000022071512233520000236630ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef NIRI_DESKTOP_ENV_H_ #define NIRI_DESKTOP_ENV_H_ #include "desktop_env.h" namespace crystaldock { class NiriDesktopEnv : public DesktopEnv { public: bool isTiling() const override { return true; } std::vector getApplicationMenuSystemCategories() const override; std::vector getDefaultLaunchers() const override; }; } // namespace crystaldock #endif // NIRI_DESKTOP_ENV_H_ dangvd-crystal-dock-7a34e96/src/desktop/sway_desktop_env.cc000066400000000000000000000030361512233520000240440ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "sway_desktop_env.h" #include namespace crystaldock { std::vector SwayDesktopEnv::getApplicationMenuSystemCategories() const { static const std::vector kSystemCategories = { {"Session", "Session", "system-log-out", { {kLockScreenId, "Lock Screen", "", "system-lock-screen", "swaylock", "" }, {kLogOutId, "Log Out", "", "system-log-out", "swaymsg exit", "" }, }, }, }; return kSystemCategories; } std::vector SwayDesktopEnv::getDefaultLaunchers() const { return { defaultWebBrowser(), "foot", kSeparatorId, kLockScreenId, kLogOutId, kSeparatorId }; } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/desktop/sway_desktop_env.h000066400000000000000000000022571512233520000237120ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTAL_DOCK_SWAY_DESKTOP_ENV_H_ #define CRYSTAL_DOCK_SWAY_DESKTOP_ENV_H_ #include "desktop_env.h" namespace crystaldock { class SwayDesktopEnv : public DesktopEnv { public: bool isTiling() const override { return true; } std::vector getApplicationMenuSystemCategories() const override; std::vector getDefaultLaunchers() const override; }; } // namespace crystaldock #endif // CRYSTAL_DOCK_SWAY_DESKTOP_ENV_H_ dangvd-crystal-dock-7a34e96/src/desktop/wayfire_desktop_env.cc000066400000000000000000000030671512233520000245330ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "wayfire_desktop_env.h" #include namespace crystaldock { std::vector WayfireDesktopEnv::getApplicationMenuSystemCategories() const { static const std::vector kSystemCategories = { {"Session", "Session", "system-log-out", { {kLockScreenId, "Lock Screen", "", "system-lock-screen", "swaylock", "" }, {kLogOutId, "Log Out", "", "system-log-out", "wlogout", "" }, }, }, }; return kSystemCategories; } std::vector WayfireDesktopEnv::getDefaultLaunchers() const { return { kShowDesktopId, defaultWebBrowser(), "alacritty", kSeparatorId, kLockScreenId, kLogOutId, kSeparatorId }; } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/desktop/wayfire_desktop_env.h000066400000000000000000000021401512233520000243640ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef WAYFIRE_DESKTOP_ENV_H_ #define WAYFIRE_DESKTOP_ENV_H_ #include "desktop_env.h" namespace crystaldock { class WayfireDesktopEnv : public DesktopEnv { public: std::vector getApplicationMenuSystemCategories() const override; std::vector getDefaultLaunchers() const override; }; } // namespace crystaldock #endif // WAYFIRE_DESKTOP_ENV_H_ dangvd-crystal-dock-7a34e96/src/display/000077500000000000000000000000001512233520000201435ustar00rootroot00000000000000dangvd-crystal-dock-7a34e96/src/display/kde_auto_hide_manager.cc000066400000000000000000000053401512233520000247320ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include #include #include "kde_auto_hide_manager.h" namespace crystaldock { kde_screen_edge_manager_v1* KdeAutoHideManager::screen_edge_manager_; /* static */ KdeAutoHideManager* KdeAutoHideManager::self() { static KdeAutoHideManager self; return &self; } /* static */ void KdeAutoHideManager::init( struct kde_screen_edge_manager_v1* screen_edge_manager) { screen_edge_manager_ = screen_edge_manager; } /* static */ void KdeAutoHideManager::bindAutoHideManagerFunctions( AutoHideManager* autoHideManager) { autoHideManager->setAutoHide = KdeAutoHideManager::setAutoHide; } /* static */ void KdeAutoHideManager::setAutoHide(QWidget* widget, Qt::Edge edge, bool on) { auto* window = WindowSystem::getWindow(widget); if (!window) { return; } auto* waylandWindow = window->nativeInterface(); if (!waylandWindow) { std::cerr << "Failed to get Wayland window" << std::endl; return; } auto* surface = waylandWindow->surface(); if (!surface) { std::cerr << "Failed to get Wayland surface" << std::endl; return; } kde_screen_edge_manager_v1_border border = KDE_SCREEN_EDGE_MANAGER_V1_BORDER_BOTTOM; switch (edge) { case Qt::TopEdge: border = KDE_SCREEN_EDGE_MANAGER_V1_BORDER_TOP; break; case Qt::BottomEdge: border = KDE_SCREEN_EDGE_MANAGER_V1_BORDER_BOTTOM; break; case Qt::LeftEdge: border = KDE_SCREEN_EDGE_MANAGER_V1_BORDER_LEFT; break; case Qt::RightEdge: border = KDE_SCREEN_EDGE_MANAGER_V1_BORDER_RIGHT; break; } auto* screen_edge = kde_screen_edge_manager_v1_get_auto_hide_screen_edge( screen_edge_manager_, border, surface); if (!screen_edge) { std::cerr << "Failed to get Auto Hide screen edge object" << std::endl; return; } if (on) { kde_auto_hide_screen_edge_v1_activate(screen_edge); } else { kde_auto_hide_screen_edge_v1_deactivate(screen_edge); } } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/display/kde_auto_hide_manager.h000066400000000000000000000025421512233520000245750ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef KDE_AUTO_HIDE_MANAGER_H_ #define KDE_AUTO_HIDE_MANAGER_H_ #include "kde_screen_edge.h" #include "window_system.h" namespace crystaldock { class KdeAutoHideManager : public QObject { Q_OBJECT private: KdeAutoHideManager() = default; public: static KdeAutoHideManager* self(); static void init(struct kde_screen_edge_manager_v1* screen_edge_manager); static void bindAutoHideManagerFunctions(AutoHideManager* autoHideManager); static void setAutoHide(QWidget* widget, Qt::Edge edge, bool on); private: static kde_screen_edge_manager_v1* screen_edge_manager_; }; } #endif // KDE_AUTO_HIDE_MANAGER_H_ dangvd-crystal-dock-7a34e96/src/display/kde_screen_edge.c000066400000000000000000000030041512233520000233720ustar00rootroot00000000000000/* Generated by wayland-scanner 1.23.0 */ /* * SPDX-FileCopyrightText: 2023 Vlad Zahorodnii * * SPDX-License-Identifier: MIT-CMU */ #include #include #include #include "wayland-util.h" #ifndef __has_attribute # define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) #define WL_PRIVATE __attribute__ ((visibility("hidden"))) #else #define WL_PRIVATE #endif extern const struct wl_interface kde_auto_hide_screen_edge_v1_interface; extern const struct wl_interface wl_surface_interface; static const struct wl_interface *kde_screen_edge_v1_types[] = { &kde_auto_hide_screen_edge_v1_interface, NULL, &wl_surface_interface, }; static const struct wl_message kde_screen_edge_manager_v1_requests[] = { { "destroy", "", kde_screen_edge_v1_types + 0 }, { "get_auto_hide_screen_edge", "nuo", kde_screen_edge_v1_types + 0 }, }; WL_PRIVATE const struct wl_interface kde_screen_edge_manager_v1_interface = { "kde_screen_edge_manager_v1", 1, 2, kde_screen_edge_manager_v1_requests, 0, NULL, }; static const struct wl_message kde_auto_hide_screen_edge_v1_requests[] = { { "destroy", "", kde_screen_edge_v1_types + 0 }, { "deactivate", "", kde_screen_edge_v1_types + 0 }, { "activate", "", kde_screen_edge_v1_types + 0 }, }; WL_PRIVATE const struct wl_interface kde_auto_hide_screen_edge_v1_interface = { "kde_auto_hide_screen_edge_v1", 1, 3, kde_auto_hide_screen_edge_v1_requests, 0, NULL, }; dangvd-crystal-dock-7a34e96/src/display/kde_screen_edge.h000066400000000000000000000257771512233520000234240ustar00rootroot00000000000000/* Generated by wayland-scanner 1.23.0 */ #ifndef KDE_SCREEN_EDGE_V1_CLIENT_PROTOCOL_H #define KDE_SCREEN_EDGE_V1_CLIENT_PROTOCOL_H #include #include #include "wayland-client.h" #ifdef __cplusplus extern "C" { #endif /** * @page page_kde_screen_edge_v1 The kde_screen_edge_v1 protocol * @section page_ifaces_kde_screen_edge_v1 Interfaces * - @subpage page_iface_kde_screen_edge_manager_v1 - screen edge manager * - @subpage page_iface_kde_auto_hide_screen_edge_v1 - auto hide screen edge * @section page_copyright_kde_screen_edge_v1 Copyright *

 *
 * SPDX-FileCopyrightText: 2023 Vlad Zahorodnii
 *
 * SPDX-License-Identifier: MIT-CMU
 * 
*/ struct kde_auto_hide_screen_edge_v1; struct kde_screen_edge_manager_v1; struct wl_surface; #ifndef KDE_SCREEN_EDGE_MANAGER_V1_INTERFACE #define KDE_SCREEN_EDGE_MANAGER_V1_INTERFACE /** * @page page_iface_kde_screen_edge_manager_v1 kde_screen_edge_manager_v1 * @section page_iface_kde_screen_edge_manager_v1_desc Description * * This interface allows clients to associate actions with screen edges. For * example, showing a surface by moving the pointer to a screen edge. * * Potential ways to trigger the screen edge are subject to compositor * policies. As an example, the compositor may consider the screen edge to be * triggered if the pointer hits its associated screen border. Other ways may * include using touchscreen or touchpad gestures. * * Warning! The protocol described in this file is a desktop environment * implementation detail. Regular clients must not use this protocol. * Backward incompatible changes may be added without bumping the major * version of the extension. * @section page_iface_kde_screen_edge_manager_v1_api API * See @ref iface_kde_screen_edge_manager_v1. */ /** * @defgroup iface_kde_screen_edge_manager_v1 The kde_screen_edge_manager_v1 interface * * This interface allows clients to associate actions with screen edges. For * example, showing a surface by moving the pointer to a screen edge. * * Potential ways to trigger the screen edge are subject to compositor * policies. As an example, the compositor may consider the screen edge to be * triggered if the pointer hits its associated screen border. Other ways may * include using touchscreen or touchpad gestures. * * Warning! The protocol described in this file is a desktop environment * implementation detail. Regular clients must not use this protocol. * Backward incompatible changes may be added without bumping the major * version of the extension. */ extern const struct wl_interface kde_screen_edge_manager_v1_interface; #endif #ifndef KDE_AUTO_HIDE_SCREEN_EDGE_V1_INTERFACE #define KDE_AUTO_HIDE_SCREEN_EDGE_V1_INTERFACE /** * @page page_iface_kde_auto_hide_screen_edge_v1 kde_auto_hide_screen_edge_v1 * @section page_iface_kde_auto_hide_screen_edge_v1_desc Description * * The auto hide screen edge object allows to hide the surface and make it * visible by triggering the screen edge. The screen edge is inactive and * the surface is visible by default. * * This interface can be used to implement user interface elements such as * auto-hide panels or docks. * * kde_auto_hide_screen_edge_v1.activate activates the screen edge and makes * the surface hidden. The surface can be made visible by triggering the * screen edge or calling kde_auto_hide_screen_edge_v1.deactivate. * * If the screen edge has been triggered, it won't be re-activated again. * Another kde_auto_hide_screen_edge_v1.activate request must be made by the * client to activate the screen edge. * @section page_iface_kde_auto_hide_screen_edge_v1_api API * See @ref iface_kde_auto_hide_screen_edge_v1. */ /** * @defgroup iface_kde_auto_hide_screen_edge_v1 The kde_auto_hide_screen_edge_v1 interface * * The auto hide screen edge object allows to hide the surface and make it * visible by triggering the screen edge. The screen edge is inactive and * the surface is visible by default. * * This interface can be used to implement user interface elements such as * auto-hide panels or docks. * * kde_auto_hide_screen_edge_v1.activate activates the screen edge and makes * the surface hidden. The surface can be made visible by triggering the * screen edge or calling kde_auto_hide_screen_edge_v1.deactivate. * * If the screen edge has been triggered, it won't be re-activated again. * Another kde_auto_hide_screen_edge_v1.activate request must be made by the * client to activate the screen edge. */ extern const struct wl_interface kde_auto_hide_screen_edge_v1_interface; #endif #ifndef KDE_SCREEN_EDGE_MANAGER_V1_ERROR_ENUM #define KDE_SCREEN_EDGE_MANAGER_V1_ERROR_ENUM enum kde_screen_edge_manager_v1_error { /** * the specified border value is invalid */ KDE_SCREEN_EDGE_MANAGER_V1_ERROR_INVALID_BORDER = 0, /** * the surface has invalid role */ KDE_SCREEN_EDGE_MANAGER_V1_ERROR_INVALID_ROLE = 1, /** * the surface already has a screen edge */ KDE_SCREEN_EDGE_MANAGER_V1_ERROR_ALREADY_CONSTRUCTED = 2, }; #endif /* KDE_SCREEN_EDGE_MANAGER_V1_ERROR_ENUM */ #ifndef KDE_SCREEN_EDGE_MANAGER_V1_BORDER_ENUM #define KDE_SCREEN_EDGE_MANAGER_V1_BORDER_ENUM /** * @ingroup iface_kde_screen_edge_manager_v1 * screen border * * These values describe possible screen borders. */ enum kde_screen_edge_manager_v1_border { /** * top screen edge */ KDE_SCREEN_EDGE_MANAGER_V1_BORDER_TOP = 1, /** * bottom screen edge */ KDE_SCREEN_EDGE_MANAGER_V1_BORDER_BOTTOM = 2, /** * left screen edge */ KDE_SCREEN_EDGE_MANAGER_V1_BORDER_LEFT = 3, /** * right screen edge */ KDE_SCREEN_EDGE_MANAGER_V1_BORDER_RIGHT = 4, }; #endif /* KDE_SCREEN_EDGE_MANAGER_V1_BORDER_ENUM */ #define KDE_SCREEN_EDGE_MANAGER_V1_DESTROY 0 #define KDE_SCREEN_EDGE_MANAGER_V1_GET_AUTO_HIDE_SCREEN_EDGE 1 /** * @ingroup iface_kde_screen_edge_manager_v1 */ #define KDE_SCREEN_EDGE_MANAGER_V1_DESTROY_SINCE_VERSION 1 /** * @ingroup iface_kde_screen_edge_manager_v1 */ #define KDE_SCREEN_EDGE_MANAGER_V1_GET_AUTO_HIDE_SCREEN_EDGE_SINCE_VERSION 1 /** @ingroup iface_kde_screen_edge_manager_v1 */ static inline void kde_screen_edge_manager_v1_set_user_data(struct kde_screen_edge_manager_v1 *kde_screen_edge_manager_v1, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) kde_screen_edge_manager_v1, user_data); } /** @ingroup iface_kde_screen_edge_manager_v1 */ static inline void * kde_screen_edge_manager_v1_get_user_data(struct kde_screen_edge_manager_v1 *kde_screen_edge_manager_v1) { return wl_proxy_get_user_data((struct wl_proxy *) kde_screen_edge_manager_v1); } static inline uint32_t kde_screen_edge_manager_v1_get_version(struct kde_screen_edge_manager_v1 *kde_screen_edge_manager_v1) { return wl_proxy_get_version((struct wl_proxy *) kde_screen_edge_manager_v1); } /** * @ingroup iface_kde_screen_edge_manager_v1 * * Destroy the screen edge manager. This doesn't destroy objects created * with this manager. */ static inline void kde_screen_edge_manager_v1_destroy(struct kde_screen_edge_manager_v1 *kde_screen_edge_manager_v1) { wl_proxy_marshal_flags((struct wl_proxy *) kde_screen_edge_manager_v1, KDE_SCREEN_EDGE_MANAGER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) kde_screen_edge_manager_v1), WL_MARSHAL_FLAG_DESTROY); } /** * @ingroup iface_kde_screen_edge_manager_v1 * * Create a new auto hide screen edge object associated with the specified * surface and the border. * * Creating a kde_auto_hide_screen_edge_v1 object does not change the * visibility of the surface. The kde_auto_hide_screen_edge_v1.activate * request must be issued in order to hide the surface. * * The "border" argument must be a valid enum entry, otherwise the * invalid_border protocol error is raised. * * The invalid_role protocol error will be raised if the specified surface * does not have layer_surface role. */ static inline struct kde_auto_hide_screen_edge_v1 * kde_screen_edge_manager_v1_get_auto_hide_screen_edge(struct kde_screen_edge_manager_v1 *kde_screen_edge_manager_v1, uint32_t border, struct wl_surface *surface) { struct wl_proxy *id; id = wl_proxy_marshal_flags((struct wl_proxy *) kde_screen_edge_manager_v1, KDE_SCREEN_EDGE_MANAGER_V1_GET_AUTO_HIDE_SCREEN_EDGE, &kde_auto_hide_screen_edge_v1_interface, wl_proxy_get_version((struct wl_proxy *) kde_screen_edge_manager_v1), 0, NULL, border, surface); return (struct kde_auto_hide_screen_edge_v1 *) id; } #define KDE_AUTO_HIDE_SCREEN_EDGE_V1_DESTROY 0 #define KDE_AUTO_HIDE_SCREEN_EDGE_V1_DEACTIVATE 1 #define KDE_AUTO_HIDE_SCREEN_EDGE_V1_ACTIVATE 2 /** * @ingroup iface_kde_auto_hide_screen_edge_v1 */ #define KDE_AUTO_HIDE_SCREEN_EDGE_V1_DESTROY_SINCE_VERSION 1 /** * @ingroup iface_kde_auto_hide_screen_edge_v1 */ #define KDE_AUTO_HIDE_SCREEN_EDGE_V1_DEACTIVATE_SINCE_VERSION 1 /** * @ingroup iface_kde_auto_hide_screen_edge_v1 */ #define KDE_AUTO_HIDE_SCREEN_EDGE_V1_ACTIVATE_SINCE_VERSION 1 /** @ingroup iface_kde_auto_hide_screen_edge_v1 */ static inline void kde_auto_hide_screen_edge_v1_set_user_data(struct kde_auto_hide_screen_edge_v1 *kde_auto_hide_screen_edge_v1, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) kde_auto_hide_screen_edge_v1, user_data); } /** @ingroup iface_kde_auto_hide_screen_edge_v1 */ static inline void * kde_auto_hide_screen_edge_v1_get_user_data(struct kde_auto_hide_screen_edge_v1 *kde_auto_hide_screen_edge_v1) { return wl_proxy_get_user_data((struct wl_proxy *) kde_auto_hide_screen_edge_v1); } static inline uint32_t kde_auto_hide_screen_edge_v1_get_version(struct kde_auto_hide_screen_edge_v1 *kde_auto_hide_screen_edge_v1) { return wl_proxy_get_version((struct wl_proxy *) kde_auto_hide_screen_edge_v1); } /** * @ingroup iface_kde_auto_hide_screen_edge_v1 * * Destroy the auto hide screen edge object. If the screen edge is active, * it will be deactivated and the surface will be made visible. */ static inline void kde_auto_hide_screen_edge_v1_destroy(struct kde_auto_hide_screen_edge_v1 *kde_auto_hide_screen_edge_v1) { wl_proxy_marshal_flags((struct wl_proxy *) kde_auto_hide_screen_edge_v1, KDE_AUTO_HIDE_SCREEN_EDGE_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) kde_auto_hide_screen_edge_v1), WL_MARSHAL_FLAG_DESTROY); } /** * @ingroup iface_kde_auto_hide_screen_edge_v1 * * Deactivate the screen edge. The surface will be made visible. */ static inline void kde_auto_hide_screen_edge_v1_deactivate(struct kde_auto_hide_screen_edge_v1 *kde_auto_hide_screen_edge_v1) { wl_proxy_marshal_flags((struct wl_proxy *) kde_auto_hide_screen_edge_v1, KDE_AUTO_HIDE_SCREEN_EDGE_V1_DEACTIVATE, NULL, wl_proxy_get_version((struct wl_proxy *) kde_auto_hide_screen_edge_v1), 0); } /** * @ingroup iface_kde_auto_hide_screen_edge_v1 * * Activate the screen edge. The surface will be hidden until the screen * edge is triggered. */ static inline void kde_auto_hide_screen_edge_v1_activate(struct kde_auto_hide_screen_edge_v1 *kde_auto_hide_screen_edge_v1) { wl_proxy_marshal_flags((struct wl_proxy *) kde_auto_hide_screen_edge_v1, KDE_AUTO_HIDE_SCREEN_EDGE_V1_ACTIVATE, NULL, wl_proxy_get_version((struct wl_proxy *) kde_auto_hide_screen_edge_v1), 0); } #ifdef __cplusplus } #endif #endif dangvd-crystal-dock-7a34e96/src/display/kde_virtual_desktop_manager.cc000066400000000000000000000147651512233520000262230ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "kde_virtual_desktop_manager.h" namespace crystaldock { org_kde_plasma_virtual_desktop_management* KdeVirtualDesktopManager::virtual_desktop_management_; std::vector KdeVirtualDesktopManager::desktops_; std::string KdeVirtualDesktopManager::currentDesktop_; /* static */ KdeVirtualDesktopManager* KdeVirtualDesktopManager::self() { static KdeVirtualDesktopManager self; return &self; } /* static */ void KdeVirtualDesktopManager::init( struct org_kde_plasma_virtual_desktop_management* virtual_desktop_management) { virtual_desktop_management_ = virtual_desktop_management; org_kde_plasma_virtual_desktop_management_add_listener( virtual_desktop_management_, &virtual_desktop_management_listener_, NULL); connect(KdeVirtualDesktopManager::self(), &KdeVirtualDesktopManager::currentDesktopChanged, WindowSystem::self(), &WindowSystem::currentDesktopChanged); connect(KdeVirtualDesktopManager::self(), &KdeVirtualDesktopManager::desktopNameChanged, WindowSystem::self(), &WindowSystem::desktopNameChanged); connect(KdeVirtualDesktopManager::self(), &KdeVirtualDesktopManager::numberOfDesktopsChanged, WindowSystem::self(), &WindowSystem::numberOfDesktopsChanged); } /* static */ void KdeVirtualDesktopManager::bindVirtualDesktopManagerFunctions( VirtualDesktopManager* virtualDesktopManager) { virtualDesktopManager->currentDesktop = KdeVirtualDesktopManager::currentDesktop; virtualDesktopManager->desktops = KdeVirtualDesktopManager::desktops; virtualDesktopManager->numberOfDesktops = KdeVirtualDesktopManager::numberOfDesktops; virtualDesktopManager->setCurrentDesktop = KdeVirtualDesktopManager::setCurrentDesktop; } /* static */ int KdeVirtualDesktopManager::numberOfDesktops() { return desktops_.size(); } /* static */ std::string_view KdeVirtualDesktopManager::currentDesktop() { return currentDesktop_; } /* static */ void KdeVirtualDesktopManager::setCurrentDesktop(std::string_view desktopId) { auto pos = std::find_if(desktops_.begin(), desktops_.end(), [desktopId](auto& e) { return e.id == desktopId; }); if (pos != desktops_.end()) { auto desktop = static_cast(pos->virtual_desktop); if (desktop != nullptr) { org_kde_plasma_virtual_desktop_request_activate(desktop); } } } // org_kde_plasma_virtual_desktop_management interface. /* static */ void KdeVirtualDesktopManager::desktop_management_desktop_created( void* data, org_kde_plasma_virtual_desktop_management* virtual_desktop_management, const char* desktop_id, uint32_t position) { VirtualDesktopInfo info; info.id = desktop_id; info.number = position + 1; auto* virtual_desktop = org_kde_plasma_virtual_desktop_management_get_virtual_desktop( virtual_desktop_management, desktop_id); info.virtual_desktop = virtual_desktop; desktops_.insert(desktops_.begin() + position, info); org_kde_plasma_virtual_desktop_add_listener( virtual_desktop, &virtual_desktop_listener_, NULL); emit self()->numberOfDesktopsChanged(desktops_.size()); } /* static */ void KdeVirtualDesktopManager::desktop_management_desktop_removed( void* data, org_kde_plasma_virtual_desktop_management* virtual_desktop_management, const char* desktop_id) { desktops_.erase(std::find_if(desktops_.begin(), desktops_.end(), [desktop_id](auto& e) { return e.id == desktop_id; })); for (unsigned int pos = 0; pos < desktops_.size(); ++pos) { desktops_[pos].number = pos + 1; } emit self()->numberOfDesktopsChanged(desktops_.size()); } /* static */ void KdeVirtualDesktopManager::desktop_management_done(void *data, struct org_kde_plasma_virtual_desktop_management* virtual_desktop_management) { // Ignore. } /* static */ void KdeVirtualDesktopManager::desktop_management_desktop_rows( void* data, org_kde_plasma_virtual_desktop_management* virtual_desktop_management, uint32_t rows) { // Ignore. } // org_kde_plasma_virtual_desktop interface. /* static */ void KdeVirtualDesktopManager::desktop_id( void *data, struct org_kde_plasma_virtual_desktop *virtual_desktop, const char *desktop_id) { auto pos = std::find_if(desktops_.begin(), desktops_.end(), [virtual_desktop](auto& e) { return e.virtual_desktop == virtual_desktop; }); if (pos != desktops_.end()) { pos->id = desktop_id; } } /* static */ void KdeVirtualDesktopManager::desktop_name( void *data, struct org_kde_plasma_virtual_desktop *virtual_desktop, const char *name) { auto pos = std::find_if(desktops_.begin(), desktops_.end(), [virtual_desktop](auto& e) { return e.virtual_desktop == virtual_desktop; }); if (pos != desktops_.end()) { pos->name = name; emit KdeVirtualDesktopManager::self()->desktopNameChanged(pos->id, pos->name); } } /* static */ void KdeVirtualDesktopManager::desktop_activated( void *data, struct org_kde_plasma_virtual_desktop *virtual_desktop) { auto pos = std::find_if(desktops_.begin(), desktops_.end(), [virtual_desktop](auto& e) { return e.virtual_desktop == virtual_desktop; }); if (pos != desktops_.end()) { if (currentDesktop_ != pos->id) { currentDesktop_ = pos->id; emit self()->currentDesktopChanged(currentDesktop_); } } } /* static */ void KdeVirtualDesktopManager::desktop_deactivated( void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop) { // Ignore. } /* static */ void KdeVirtualDesktopManager::desktop_done( void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop) { // Ignore. } /* static */ void KdeVirtualDesktopManager::desktop_removed( void *data, struct org_kde_plasma_virtual_desktop *virtual_desktop) { // Ignore. } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/display/kde_virtual_desktop_manager.h000066400000000000000000000077551512233520000260660ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef KDE_VIRTUAL_DESKTOP_MANAGER_H_ #define KDE_VIRTUAL_DESKTOP_MANAGER_H_ #include #include "plasma_virtual_desktop.h" #include "window_system.h" namespace crystaldock { class KdeVirtualDesktopManager : public QObject { Q_OBJECT private: KdeVirtualDesktopManager() = default; signals: void currentDesktopChanged(std::string_view); void numberOfDesktopsChanged(int); void desktopNameChanged(std::string_view desktopId, std::string_view desktopName); public: static KdeVirtualDesktopManager* self(); static void init(struct org_kde_plasma_virtual_desktop_management* virtual_desktop_management); static void bindVirtualDesktopManagerFunctions(VirtualDesktopManager* virtualDesktopManager); static int numberOfDesktops(); static std::vector desktops() { return desktops_; } static std::string_view currentDesktop(); static void setCurrentDesktop(std::string_view); private: // org_kde_plasma_virtual_desktop_management interface. static void desktop_management_desktop_created( void* data, org_kde_plasma_virtual_desktop_management* virtual_desktop_management, const char* desktop_id, uint32_t position); static void desktop_management_desktop_removed( void* data, org_kde_plasma_virtual_desktop_management* virtual_desktop_management, const char* desktop_id); static void desktop_management_done(void *data, struct org_kde_plasma_virtual_desktop_management* virtual_desktop_management); static void desktop_management_desktop_rows( void* data, org_kde_plasma_virtual_desktop_management* virtual_desktop_management, uint32_t rows); // org_kde_plasma_virtual_desktop interface. static void desktop_id( void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop, const char *desktop_id); static void desktop_name( void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop, const char *name); static void desktop_activated( void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop); static void desktop_deactivated( void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop); static void desktop_done( void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop); static void desktop_removed( void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop); static constexpr struct org_kde_plasma_virtual_desktop_management_listener virtual_desktop_management_listener_ = { desktop_management_desktop_created, desktop_management_desktop_removed, desktop_management_done, desktop_management_desktop_rows }; static constexpr struct org_kde_plasma_virtual_desktop_listener virtual_desktop_listener_ = { desktop_id, desktop_name, desktop_activated, desktop_deactivated, desktop_done, desktop_removed }; static org_kde_plasma_virtual_desktop_management* virtual_desktop_management_; static std::vector desktops_; // Current desktop ID. static std::string currentDesktop_; }; } #endif // KDE_VIRTUAL_DESKTOP_MANAGER_H_ dangvd-crystal-dock-7a34e96/src/display/kde_window_manager.cc000066400000000000000000000334551512233520000243100ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "kde_window_manager.h" namespace crystaldock { org_kde_plasma_window_management* KdeWindowManager::window_management_; std::unordered_map> KdeWindowManager::windows_; std::unordered_map KdeWindowManager::uuids_; std::vector KdeWindowManager::stackingOrder_; struct org_kde_plasma_window* KdeWindowManager::activeWindow_; bool KdeWindowManager::showingDesktop_; /* static */ KdeWindowManager* KdeWindowManager::self() { static KdeWindowManager self; return &self; } /* static */ void KdeWindowManager::init( struct org_kde_plasma_window_management* window_management) { window_management_ = window_management; org_kde_plasma_window_management_add_listener( window_management_, &window_management_listener_, NULL); connect(KdeWindowManager::self(), &KdeWindowManager::activeWindowChanged, WindowSystem::self(), &WindowSystem::activeWindowChanged); connect(KdeWindowManager::self(), &KdeWindowManager::windowAdded, WindowSystem::self(), &WindowSystem::windowAdded); connect(KdeWindowManager::self(), &KdeWindowManager::windowGeometryChanged, WindowSystem::self(), &WindowSystem::windowGeometryChanged); connect(KdeWindowManager::self(), &KdeWindowManager::windowLeftCurrentActivity, WindowSystem::self(), &WindowSystem::windowLeftCurrentActivity); connect(KdeWindowManager::self(), &KdeWindowManager::windowLeftCurrentDesktop, WindowSystem::self(), &WindowSystem::windowLeftCurrentDesktop); connect(KdeWindowManager::self(), &KdeWindowManager::windowRemoved, WindowSystem::self(), &WindowSystem::windowRemoved); connect(KdeWindowManager::self(), &KdeWindowManager::windowStateChanged, WindowSystem::self(), &WindowSystem::windowStateChanged); connect(KdeWindowManager::self(), &KdeWindowManager::windowTitleChanged, WindowSystem::self(), &WindowSystem::windowTitleChanged); } /* static */ void KdeWindowManager::bindWindowManagerFunctions( WindowManager* windowManager) { windowManager->activateOrMinimizeWindow = KdeWindowManager::activateOrMinimizeWindow; windowManager->activateWindow = KdeWindowManager::activateWindow; windowManager->minimizeWindow = KdeWindowManager::minimizeWindow; windowManager->activeWindow = KdeWindowManager::activeWindow; windowManager->closeWindow = KdeWindowManager::closeWindow; windowManager->resetActiveWindow = KdeWindowManager::resetActiveWindow; windowManager->windows = KdeWindowManager::windows; windowManager->setShowingDesktop = KdeWindowManager::setShowingDesktop; windowManager->showingDesktop = KdeWindowManager::showingDesktop; } /* static */ std::vector KdeWindowManager::windows() { std::vector windows; for (const auto& element : windows_) { if (element.second) { windows.push_back(element.second.get()); } } std::sort(windows.begin(), windows.end(), [](const WindowInfo* w1, const WindowInfo* w2) { return w1->mapping_order < w2->mapping_order; }); return windows; } /* static */ void* KdeWindowManager::activeWindow() { return activeWindow_; } /* static */ void KdeWindowManager::resetActiveWindow() { activeWindow_ = nullptr; emit KdeWindowManager::self()->activeWindowChanged(nullptr); } /* static */ void KdeWindowManager::activateWindow(void* window_handle) { auto* window = static_cast(window_handle); if (window) { org_kde_plasma_window_set_state( window, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ACTIVE, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ACTIVE); } } /* static */ void KdeWindowManager::activateOrMinimizeWindow(void* window_handle) { auto* window = static_cast(window_handle); if (window) { if (windows_.count(window) == 0) { return; } if (windows_[window]->minimized || window != activeWindow_) { org_kde_plasma_window_set_state( window, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ACTIVE, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ACTIVE); } else { org_kde_plasma_window_set_state( window, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MINIMIZED, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MINIMIZED); } } } /* static */ void KdeWindowManager::minimizeWindow(void* window_handle) { auto* window = static_cast(window_handle); if (window) { org_kde_plasma_window_set_state( window, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MINIMIZED, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MINIMIZED); } } /*static*/ void KdeWindowManager::closeWindow(void* window_handle) { auto* window = static_cast(window_handle); if (window) { org_kde_plasma_window_close(window); } } /* static */ bool KdeWindowManager::showingDesktop() { return showingDesktop_; } /* static */ void KdeWindowManager::setShowingDesktop(bool show) { // This does not work because it would hide Crystal Dock. /* org_kde_plasma_window_management_show_desktop( window_management_, show ? ORG_KDE_PLASMA_WINDOW_MANAGEMENT_SHOW_DESKTOP_ENABLED : ORG_KDE_PLASMA_WINDOW_MANAGEMENT_SHOW_DESKTOP_DISABLED); */ // So we make our own implementation. for (const auto& uuid : stackingOrder_) { auto* window = uuids_[uuid]; if (windows_[window]->desktop != WindowSystem::currentDesktop()) { continue; } if (show) { windows_[window]->restoreAfterShowDesktop = !windows_[window]->minimized; if (!windows_[window]->minimized) { org_kde_plasma_window_set_state( window, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MINIMIZED, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MINIMIZED); } showingDesktop_ = true; } else { if (windows_[window]->restoreAfterShowDesktop) { org_kde_plasma_window_set_state( window, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ACTIVE, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ACTIVE); } showingDesktop_ = false; } } } // org_kde_plasma_window_management interface. /* static */ void KdeWindowManager::show_desktop_changed( void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, uint32_t state) { // Ignore and use our own state. //showingDesktop_ = state & ORG_KDE_PLASMA_WINDOW_MANAGEMENT_SHOW_DESKTOP_ENABLED; } /* static */ void KdeWindowManager::window( void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, uint32_t id) { // Ignore. } /* static */ void KdeWindowManager::stacking_order_changed( void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, struct wl_array *ids) { // Ignore. } /* static */ void KdeWindowManager::stacking_order_uuid_changed( void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, const char *uuids) { QStringList ids = QString(uuids).split(";", Qt::SkipEmptyParts); stackingOrder_.clear(); for (const auto& id : ids) { stackingOrder_.push_back(id.toStdString()); } } /* static */ void KdeWindowManager::window_with_uuid( void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, uint32_t id, const char *uuid) { struct org_kde_plasma_window* window = org_kde_plasma_window_management_get_window_by_uuid(window_management_, uuid); windows_[window] = std::make_unique(); windows_[window]->window = window; static uint32_t mapping_order = 0; windows_[window]->mapping_order = mapping_order++; uuids_[std::string{uuid}] = window; org_kde_plasma_window_add_listener(window, &window_listener_, NULL); } // org_kde_plasma_window interface. /* static */ void KdeWindowManager::title_changed( void *data, struct org_kde_plasma_window* window, const char *title) { if (windows_.count(window) == 0) { return; } windows_[window]->title = title; if (windows_[window]->initialized) { emit self()->windowTitleChanged(windows_[window].get()); } } /* static */ void KdeWindowManager::app_id_changed( void *data, struct org_kde_plasma_window* window, const char *app_id) { if (windows_.count(window) == 0) { return; } windows_[window]->appId = app_id; if (std::string(app_id) == "crystal-dock") { org_kde_plasma_window_set_state( window, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ON_ALL_DESKTOPS | ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SKIPTASKBAR, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ON_ALL_DESKTOPS | ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SKIPTASKBAR); } } /* static */ void KdeWindowManager::state_changed( void *data, struct org_kde_plasma_window* window, uint32_t flags) { if (windows_.count(window) == 0) { return; } windows_[window]->skipTaskbar = flags & ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SKIPTASKBAR; windows_[window]->onAllDesktops = flags & ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ON_ALL_DESKTOPS; windows_[window]->demandsAttention = flags & ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_DEMANDS_ATTENTION; windows_[window]->minimized = flags & ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MINIMIZED; if (windows_[window]->minimized && activeWindow_ == window) { activeWindow_ = nullptr; if (windows_[window]->initialized) { emit self()->activeWindowChanged(activeWindow_); } } else if (flags & ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ACTIVE && activeWindow_ != window) { activeWindow_ = window; if (windows_[window]->initialized) { emit self()->activeWindowChanged(activeWindow_); } } if (windows_[window]->initialized) { emit self()->windowStateChanged(windows_[window].get()); } } /* static */ void KdeWindowManager::virtual_desktop_changed( void *data, struct org_kde_plasma_window* window, int32_t number) { // Ignore. } /* static */ void KdeWindowManager::themed_icon_name_changed( void *data, struct org_kde_plasma_window* window, const char *name) { if (windows_.count(window) == 0) { return; } windows_[window]->icon = name; } /* static */ void KdeWindowManager::unmapped( void *data, struct org_kde_plasma_window* window) { if (windows_.count(window) == 0) { return; } emit self()->windowRemoved(windows_[window]->window); windows_.erase(window); } /* static */ void KdeWindowManager::initial_state( void *data, struct org_kde_plasma_window* window) { if (windows_.count(window) == 0) { return; } windows_[window]->initialized = true; if (!windows_[window]->skipTaskbar) { emit self()->windowAdded(windows_[window].get()); } } /* static */ void KdeWindowManager::parent_window( void *data, struct org_kde_plasma_window* window, struct org_kde_plasma_window *parent) { } /* static */ void KdeWindowManager::geometry( void *data, struct org_kde_plasma_window* window, int32_t x, int32_t y, uint32_t width, uint32_t height) { if (windows_.count(window) == 0) { return; } windows_[window]->x = x; windows_[window]->y = y; windows_[window]->width = width; windows_[window]->height = height; if (windows_[window]->initialized) { emit self()->windowGeometryChanged(windows_[window].get()); } } /* static */ void KdeWindowManager::icon_changed( void *data, struct org_kde_plasma_window* window) {} /* static */ void KdeWindowManager::pid_changed( void *data, struct org_kde_plasma_window* window, uint32_t pid) {} /* static */ void KdeWindowManager::virtual_desktop_entered( void *data, struct org_kde_plasma_window* window, const char *id) { if (windows_.count(window) == 0) { return; } windows_[window]->desktop = id; } /* static */ void KdeWindowManager::virtual_desktop_left( void *data, struct org_kde_plasma_window* window, const char *id) { if (id != WindowSystem::currentDesktop()) { return; } if (windows_.count(window) == 0) { return; } if (windows_[window]->initialized && !windows_[window]->onAllDesktops) { emit self()->windowLeftCurrentDesktop(windows_[window]->window); } } /* static */ void KdeWindowManager::application_menu( void *data, struct org_kde_plasma_window* window, const char *service_name, const char *object_path) {} /* static */ void KdeWindowManager::activity_entered( void *data, struct org_kde_plasma_window* window, const char *id) { if (windows_.count(window) == 0) { return; } windows_[window]->activity = id; } /* static */ void KdeWindowManager::activity_left( void *data, struct org_kde_plasma_window* window, const char *id) { if (id != WindowSystem::currentActivity()) { return; } if (windows_.count(window) == 0) { return; } if (windows_[window]->initialized) { emit self()->windowLeftCurrentActivity(windows_[window]->window); } } /* static */ void KdeWindowManager::resource_name_changed( void *data, struct org_kde_plasma_window* window, const char *resource_name) {} } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/display/kde_window_manager.h000066400000000000000000000150161512233520000241430ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef KDE_WINDOW_MANAGER_H_ #define KDE_WINDOW_MANAGER_H_ #include #include #include "plasma_window_management.h" #include "window_system.h" namespace crystaldock { class KdeWindowManager : public QObject { Q_OBJECT private: KdeWindowManager() = default; signals: void windowAdded(const WindowInfo*); void windowRemoved(void*); void windowLeftCurrentDesktop(void*); void windowGeometryChanged(const WindowInfo*); void windowStateChanged(const WindowInfo*); void windowTitleChanged(const WindowInfo*); void activeWindowChanged(void*); void windowLeftCurrentActivity(void*); public: static KdeWindowManager* self(); static void init(struct org_kde_plasma_window_management* window_management); static void bindWindowManagerFunctions(WindowManager* windowManager); static std::vector windows(); static void* activeWindow(); // We manually reset active window, usually when the new active window is the dock itself. // We don't want to always do this (e.g. handle this in state_change() handler) because // otherwise we wouldn't be able to click on an active window's icon to minimize it // (the click action would change the active window to be the dock). static void resetActiveWindow(); static void activateWindow(void* window); static void activateOrMinimizeWindow(void* window); static void minimizeWindow(void* window); static void closeWindow(void* window); static bool showingDesktop(); static void setShowingDesktop(bool show); private: // org_kde_plasma_window_management interface. static void show_desktop_changed( void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, uint32_t state); static void window( void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, uint32_t id); static void stacking_order_changed( void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, struct wl_array *ids); static void stacking_order_uuid_changed( void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, const char *uuids); static void window_with_uuid( void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, uint32_t id, const char *uuid); // org_kde_plasma_window interface. static void title_changed( void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *title); static void app_id_changed( void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *app_id); static void state_changed( void *data, struct org_kde_plasma_window *org_kde_plasma_window, uint32_t flags); static void virtual_desktop_changed( void *data, struct org_kde_plasma_window *org_kde_plasma_window, int32_t number); static void themed_icon_name_changed( void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *name); static void unmapped( void *data, struct org_kde_plasma_window *org_kde_plasma_window); static void initial_state( void *data, struct org_kde_plasma_window *org_kde_plasma_window); static void parent_window( void *data, struct org_kde_plasma_window *org_kde_plasma_window, struct org_kde_plasma_window *parent); static void geometry( void *data, struct org_kde_plasma_window *org_kde_plasma_window, int32_t x, int32_t y, uint32_t width, uint32_t height); static void icon_changed( void *data, struct org_kde_plasma_window *org_kde_plasma_window); static void pid_changed( void *data, struct org_kde_plasma_window *org_kde_plasma_window, uint32_t pid); static void virtual_desktop_entered( void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *id); static void virtual_desktop_left( void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *is); static void application_menu( void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *service_name, const char *object_path); static void activity_entered( void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *id); static void activity_left( void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *id); static void resource_name_changed( void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *resource_name); static constexpr struct org_kde_plasma_window_management_listener window_management_listener_ = { show_desktop_changed, window, stacking_order_changed, stacking_order_uuid_changed, window_with_uuid }; static constexpr struct org_kde_plasma_window_listener window_listener_ = { title_changed, app_id_changed, state_changed, virtual_desktop_changed, themed_icon_name_changed, unmapped, initial_state, parent_window, geometry, icon_changed, pid_changed, virtual_desktop_entered, virtual_desktop_left, application_menu, activity_entered, activity_left, resource_name_changed, }; static org_kde_plasma_window_management* window_management_; static std::unordered_map> windows_; static std::unordered_map uuids_; static std::vector stackingOrder_; static struct org_kde_plasma_window* activeWindow_; static bool showingDesktop_; }; } #endif // KDE_WINDOW_MANAGER_H_ dangvd-crystal-dock-7a34e96/src/display/plasma_virtual_desktop.c000066400000000000000000000046541512233520000250740ustar00rootroot00000000000000/* Generated by wayland-scanner 1.22.0 */ /* * SPDX-FileCopyrightText: 2018 Marco Martin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include #include #include "wayland-util.h" #ifndef __has_attribute # define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) #define WL_PRIVATE __attribute__ ((visibility("hidden"))) #else #define WL_PRIVATE #endif extern const struct wl_interface org_kde_plasma_virtual_desktop_interface; static const struct wl_interface *org_kde_plasma_virtual_desktop_types[] = { NULL, NULL, &org_kde_plasma_virtual_desktop_interface, NULL, }; static const struct wl_message org_kde_plasma_virtual_desktop_management_requests[] = { { "get_virtual_desktop", "ns", org_kde_plasma_virtual_desktop_types + 2 }, { "request_create_virtual_desktop", "su", org_kde_plasma_virtual_desktop_types + 0 }, { "request_remove_virtual_desktop", "s", org_kde_plasma_virtual_desktop_types + 0 }, }; static const struct wl_message org_kde_plasma_virtual_desktop_management_events[] = { { "desktop_created", "su", org_kde_plasma_virtual_desktop_types + 0 }, { "desktop_removed", "s", org_kde_plasma_virtual_desktop_types + 0 }, { "done", "", org_kde_plasma_virtual_desktop_types + 0 }, { "rows", "2u", org_kde_plasma_virtual_desktop_types + 0 }, }; WL_PRIVATE const struct wl_interface org_kde_plasma_virtual_desktop_management_interface = { "org_kde_plasma_virtual_desktop_management", 2, 3, org_kde_plasma_virtual_desktop_management_requests, 4, org_kde_plasma_virtual_desktop_management_events, }; static const struct wl_message org_kde_plasma_virtual_desktop_requests[] = { { "request_activate", "", org_kde_plasma_virtual_desktop_types + 0 }, }; static const struct wl_message org_kde_plasma_virtual_desktop_events[] = { { "desktop_id", "s", org_kde_plasma_virtual_desktop_types + 0 }, { "name", "s", org_kde_plasma_virtual_desktop_types + 0 }, { "activated", "", org_kde_plasma_virtual_desktop_types + 0 }, { "deactivated", "", org_kde_plasma_virtual_desktop_types + 0 }, { "done", "", org_kde_plasma_virtual_desktop_types + 0 }, { "removed", "", org_kde_plasma_virtual_desktop_types + 0 }, }; WL_PRIVATE const struct wl_interface org_kde_plasma_virtual_desktop_interface = { "org_kde_plasma_virtual_desktop", 1, 1, org_kde_plasma_virtual_desktop_requests, 6, org_kde_plasma_virtual_desktop_events, }; dangvd-crystal-dock-7a34e96/src/display/plasma_virtual_desktop.h000066400000000000000000000335401512233520000250750ustar00rootroot00000000000000/* Generated by wayland-scanner 1.22.0 */ #ifndef ORG_KDE_PLASMA_VIRTUAL_DESKTOP_CLIENT_PROTOCOL_H #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_CLIENT_PROTOCOL_H #include #include #include "wayland-client.h" #ifdef __cplusplus extern "C" { #endif /** * @page page_org_kde_plasma_virtual_desktop The org_kde_plasma_virtual_desktop protocol * @section page_ifaces_org_kde_plasma_virtual_desktop Interfaces * - @subpage page_iface_org_kde_plasma_virtual_desktop_management - * - @subpage page_iface_org_kde_plasma_virtual_desktop - * @section page_copyright_org_kde_plasma_virtual_desktop Copyright *
 *
 * SPDX-FileCopyrightText: 2018 Marco Martin
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 * 
*/ struct org_kde_plasma_virtual_desktop; struct org_kde_plasma_virtual_desktop_management; #ifndef ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_INTERFACE #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_INTERFACE /** * @page page_iface_org_kde_plasma_virtual_desktop_management org_kde_plasma_virtual_desktop_management * @section page_iface_org_kde_plasma_virtual_desktop_management_api API * See @ref iface_org_kde_plasma_virtual_desktop_management. */ /** * @defgroup iface_org_kde_plasma_virtual_desktop_management The org_kde_plasma_virtual_desktop_management interface */ extern const struct wl_interface org_kde_plasma_virtual_desktop_management_interface; #endif #ifndef ORG_KDE_PLASMA_VIRTUAL_DESKTOP_INTERFACE #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_INTERFACE /** * @page page_iface_org_kde_plasma_virtual_desktop org_kde_plasma_virtual_desktop * @section page_iface_org_kde_plasma_virtual_desktop_api API * See @ref iface_org_kde_plasma_virtual_desktop. */ /** * @defgroup iface_org_kde_plasma_virtual_desktop The org_kde_plasma_virtual_desktop interface */ extern const struct wl_interface org_kde_plasma_virtual_desktop_interface; #endif /** * @ingroup iface_org_kde_plasma_virtual_desktop_management * @struct org_kde_plasma_virtual_desktop_management_listener */ struct org_kde_plasma_virtual_desktop_management_listener { /** * Emitted when a new desktop has been created * * * @param desktop_id Unique id of the desktop * @param position Position of this desktop, to ensure the client and the server will see desktops in the same order */ void (*desktop_created)(void *data, struct org_kde_plasma_virtual_desktop_management *org_kde_plasma_virtual_desktop_management, const char *desktop_id, uint32_t position); /** * Emitted when a desktop has been removed * * * @param desktop_id Unique id of the desktop */ void (*desktop_removed)(void *data, struct org_kde_plasma_virtual_desktop_management *org_kde_plasma_virtual_desktop_management, const char *desktop_id); /** * sent all information about desktops * * This event is sent after all other properties has been sent * after binding to the desktop manager object and after any other * property changes done after that. This allows changes to the * org_kde_plasma_virtual_desktop_management properties to be seen * as atomic, even if they happen via multiple events. */ void (*done)(void *data, struct org_kde_plasma_virtual_desktop_management *org_kde_plasma_virtual_desktop_management); /** * @param rows Number of rows the virtual desktops are laid out into. * @since 2 */ void (*rows)(void *data, struct org_kde_plasma_virtual_desktop_management *org_kde_plasma_virtual_desktop_management, uint32_t rows); }; /** * @ingroup iface_org_kde_plasma_virtual_desktop_management */ static inline int org_kde_plasma_virtual_desktop_management_add_listener(struct org_kde_plasma_virtual_desktop_management *org_kde_plasma_virtual_desktop_management, const struct org_kde_plasma_virtual_desktop_management_listener *listener, void *data) { return wl_proxy_add_listener((struct wl_proxy *) org_kde_plasma_virtual_desktop_management, (void (**)(void)) listener, data); } #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_GET_VIRTUAL_DESKTOP 0 #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_REQUEST_CREATE_VIRTUAL_DESKTOP 1 #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_REQUEST_REMOVE_VIRTUAL_DESKTOP 2 /** * @ingroup iface_org_kde_plasma_virtual_desktop_management */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_DESKTOP_CREATED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_virtual_desktop_management */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_DESKTOP_REMOVED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_virtual_desktop_management */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_DONE_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_virtual_desktop_management */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_ROWS_SINCE_VERSION 2 /** * @ingroup iface_org_kde_plasma_virtual_desktop_management */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_GET_VIRTUAL_DESKTOP_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_virtual_desktop_management */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_REQUEST_CREATE_VIRTUAL_DESKTOP_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_virtual_desktop_management */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_REQUEST_REMOVE_VIRTUAL_DESKTOP_SINCE_VERSION 1 /** @ingroup iface_org_kde_plasma_virtual_desktop_management */ static inline void org_kde_plasma_virtual_desktop_management_set_user_data(struct org_kde_plasma_virtual_desktop_management *org_kde_plasma_virtual_desktop_management, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) org_kde_plasma_virtual_desktop_management, user_data); } /** @ingroup iface_org_kde_plasma_virtual_desktop_management */ static inline void * org_kde_plasma_virtual_desktop_management_get_user_data(struct org_kde_plasma_virtual_desktop_management *org_kde_plasma_virtual_desktop_management) { return wl_proxy_get_user_data((struct wl_proxy *) org_kde_plasma_virtual_desktop_management); } static inline uint32_t org_kde_plasma_virtual_desktop_management_get_version(struct org_kde_plasma_virtual_desktop_management *org_kde_plasma_virtual_desktop_management) { return wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_virtual_desktop_management); } /** @ingroup iface_org_kde_plasma_virtual_desktop_management */ static inline void org_kde_plasma_virtual_desktop_management_destroy(struct org_kde_plasma_virtual_desktop_management *org_kde_plasma_virtual_desktop_management) { wl_proxy_destroy((struct wl_proxy *) org_kde_plasma_virtual_desktop_management); } /** * @ingroup iface_org_kde_plasma_virtual_desktop_management * * Given the id of a particular virtual desktop, get the corresponding org_kde_plasma_virtual_desktop which represents only the desktop with that id. */ static inline struct org_kde_plasma_virtual_desktop * org_kde_plasma_virtual_desktop_management_get_virtual_desktop(struct org_kde_plasma_virtual_desktop_management *org_kde_plasma_virtual_desktop_management, const char *desktop_id) { struct wl_proxy *id; id = wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_virtual_desktop_management, ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_GET_VIRTUAL_DESKTOP, &org_kde_plasma_virtual_desktop_interface, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_virtual_desktop_management), 0, NULL, desktop_id); return (struct org_kde_plasma_virtual_desktop *) id; } /** * @ingroup iface_org_kde_plasma_virtual_desktop_management * * Ask the server to create a new virtual desktop, and position it at a specified position. If the position is zero or less, it will be positioned at the beginning, if the position is the count or more, it will be positioned at the end. */ static inline void org_kde_plasma_virtual_desktop_management_request_create_virtual_desktop(struct org_kde_plasma_virtual_desktop_management *org_kde_plasma_virtual_desktop_management, const char *name, uint32_t position) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_virtual_desktop_management, ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_REQUEST_CREATE_VIRTUAL_DESKTOP, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_virtual_desktop_management), 0, name, position); } /** * @ingroup iface_org_kde_plasma_virtual_desktop_management * * Ask the server to get rid of a virtual desktop, the server may or may not acconsent to the request. */ static inline void org_kde_plasma_virtual_desktop_management_request_remove_virtual_desktop(struct org_kde_plasma_virtual_desktop_management *org_kde_plasma_virtual_desktop_management, const char *desktop_id) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_virtual_desktop_management, ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_REQUEST_REMOVE_VIRTUAL_DESKTOP, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_virtual_desktop_management), 0, desktop_id); } /** * @ingroup iface_org_kde_plasma_virtual_desktop * @struct org_kde_plasma_virtual_desktop_listener */ struct org_kde_plasma_virtual_desktop_listener { /** * The desktop got an id * * The format of the id is decided by the compositor * implementation. A desktop id univocally identifies a virtual * desktop and must be guaranteed to never exist two desktops with * the same id. The format of the string id is up to the server * implementation. * @param desktop_id Unique id of the desktop */ void (*desktop_id)(void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop, const char *desktop_id); /** * @param name User readable descriptive name for the desktop */ void (*name)(void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop, const char *name); /** * The desktop has been activated * * The desktop will be the new "current" desktop of the system. * The server may support either one virtual desktop active at a * time, or other combinations such as one virtual desktop active * per screen. Windows associated to this virtual desktop will be * shown. */ void (*activated)(void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop); /** * This desktop is no longer active * * Windows that were associated only to this desktop will be * hidden. */ void (*deactivated)(void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop); /** * sent all information about desktops * * This event is sent after all other properties has been sent * after binding to the desktop object and after any other property * changes done after that. This allows changes to the * org_kde_plasma_virtual_desktop properties to be seen as atomic, * even if they happen via multiple events. */ void (*done)(void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop); /** * This desktop has been removed * * This virtual desktop has just been removed by the server: All * windows will lose the association to this desktop. */ void (*removed)(void *data, struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop); }; /** * @ingroup iface_org_kde_plasma_virtual_desktop */ static inline int org_kde_plasma_virtual_desktop_add_listener(struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop, const struct org_kde_plasma_virtual_desktop_listener *listener, void *data) { return wl_proxy_add_listener((struct wl_proxy *) org_kde_plasma_virtual_desktop, (void (**)(void)) listener, data); } #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_REQUEST_ACTIVATE 0 /** * @ingroup iface_org_kde_plasma_virtual_desktop */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_DESKTOP_ID_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_virtual_desktop */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_NAME_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_virtual_desktop */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_ACTIVATED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_virtual_desktop */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_DEACTIVATED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_virtual_desktop */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_DONE_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_virtual_desktop */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_REMOVED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_virtual_desktop */ #define ORG_KDE_PLASMA_VIRTUAL_DESKTOP_REQUEST_ACTIVATE_SINCE_VERSION 1 /** @ingroup iface_org_kde_plasma_virtual_desktop */ static inline void org_kde_plasma_virtual_desktop_set_user_data(struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) org_kde_plasma_virtual_desktop, user_data); } /** @ingroup iface_org_kde_plasma_virtual_desktop */ static inline void * org_kde_plasma_virtual_desktop_get_user_data(struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop) { return wl_proxy_get_user_data((struct wl_proxy *) org_kde_plasma_virtual_desktop); } static inline uint32_t org_kde_plasma_virtual_desktop_get_version(struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop) { return wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_virtual_desktop); } /** @ingroup iface_org_kde_plasma_virtual_desktop */ static inline void org_kde_plasma_virtual_desktop_destroy(struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop) { wl_proxy_destroy((struct wl_proxy *) org_kde_plasma_virtual_desktop); } /** * @ingroup iface_org_kde_plasma_virtual_desktop * * Request the server to set the status of this desktop to active: The server is free to consent or deny the request. This will be the new "current" virtual desktop of the system. */ static inline void org_kde_plasma_virtual_desktop_request_activate(struct org_kde_plasma_virtual_desktop *org_kde_plasma_virtual_desktop) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_virtual_desktop, ORG_KDE_PLASMA_VIRTUAL_DESKTOP_REQUEST_ACTIVATE, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_virtual_desktop), 0); } #ifdef __cplusplus } #endif #endif dangvd-crystal-dock-7a34e96/src/display/plasma_window_management.c000066400000000000000000000126371512233520000253600ustar00rootroot00000000000000/* Generated by wayland-scanner 1.22.0 */ /* * SPDX-FileCopyrightText: 2013-2014 Pier Luigi Fiorini * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include #include #include "wayland-util.h" #ifndef __has_attribute # define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) #define WL_PRIVATE __attribute__ ((visibility("hidden"))) #else #define WL_PRIVATE #endif extern const struct wl_interface org_kde_plasma_activation_interface; extern const struct wl_interface org_kde_plasma_window_interface; extern const struct wl_interface wl_output_interface; extern const struct wl_interface wl_surface_interface; static const struct wl_interface *plasma_window_management_types[] = { NULL, NULL, NULL, NULL, &org_kde_plasma_window_interface, NULL, &org_kde_plasma_window_interface, NULL, &wl_surface_interface, NULL, NULL, NULL, NULL, &wl_surface_interface, &wl_output_interface, &org_kde_plasma_window_interface, &org_kde_plasma_activation_interface, }; static const struct wl_message org_kde_plasma_window_management_requests[] = { { "show_desktop", "u", plasma_window_management_types + 0 }, { "get_window", "nu", plasma_window_management_types + 4 }, { "get_window_by_uuid", "12ns", plasma_window_management_types + 6 }, }; static const struct wl_message org_kde_plasma_window_management_events[] = { { "show_desktop_changed", "u", plasma_window_management_types + 0 }, { "window", "u", plasma_window_management_types + 0 }, { "stacking_order_changed", "11a", plasma_window_management_types + 0 }, { "stacking_order_uuid_changed", "12s", plasma_window_management_types + 0 }, { "window_with_uuid", "13us", plasma_window_management_types + 0 }, }; WL_PRIVATE const struct wl_interface org_kde_plasma_window_management_interface = { "org_kde_plasma_window_management", 16, 3, org_kde_plasma_window_management_requests, 5, org_kde_plasma_window_management_events, }; static const struct wl_message org_kde_plasma_window_requests[] = { { "set_state", "uu", plasma_window_management_types + 0 }, { "set_virtual_desktop", "u", plasma_window_management_types + 0 }, { "set_minimized_geometry", "ouuuu", plasma_window_management_types + 8 }, { "unset_minimized_geometry", "o", plasma_window_management_types + 13 }, { "close", "", plasma_window_management_types + 0 }, { "request_move", "3", plasma_window_management_types + 0 }, { "request_resize", "3", plasma_window_management_types + 0 }, { "destroy", "4", plasma_window_management_types + 0 }, { "get_icon", "7h", plasma_window_management_types + 0 }, { "request_enter_virtual_desktop", "8s", plasma_window_management_types + 0 }, { "request_enter_new_virtual_desktop", "8", plasma_window_management_types + 0 }, { "request_leave_virtual_desktop", "8s", plasma_window_management_types + 0 }, { "request_enter_activity", "14s", plasma_window_management_types + 0 }, { "request_leave_activity", "14s", plasma_window_management_types + 0 }, { "send_to_output", "15o", plasma_window_management_types + 14 }, }; static const struct wl_message org_kde_plasma_window_events[] = { { "title_changed", "s", plasma_window_management_types + 0 }, { "app_id_changed", "s", plasma_window_management_types + 0 }, { "state_changed", "u", plasma_window_management_types + 0 }, { "virtual_desktop_changed", "i", plasma_window_management_types + 0 }, { "themed_icon_name_changed", "s", plasma_window_management_types + 0 }, { "unmapped", "", plasma_window_management_types + 0 }, { "initial_state", "4", plasma_window_management_types + 0 }, { "parent_window", "5?o", plasma_window_management_types + 15 }, { "geometry", "6iiuu", plasma_window_management_types + 0 }, { "icon_changed", "7", plasma_window_management_types + 0 }, { "pid_changed", "u", plasma_window_management_types + 0 }, { "virtual_desktop_entered", "8s", plasma_window_management_types + 0 }, { "virtual_desktop_left", "8s", plasma_window_management_types + 0 }, { "application_menu", "10ss", plasma_window_management_types + 0 }, { "activity_entered", "14s", plasma_window_management_types + 0 }, { "activity_left", "14s", plasma_window_management_types + 0 }, { "resource_name_changed", "16s", plasma_window_management_types + 0 }, }; WL_PRIVATE const struct wl_interface org_kde_plasma_window_interface = { "org_kde_plasma_window", 16, 15, org_kde_plasma_window_requests, 17, org_kde_plasma_window_events, }; static const struct wl_message org_kde_plasma_activation_feedback_requests[] = { { "destroy", "", plasma_window_management_types + 0 }, }; static const struct wl_message org_kde_plasma_activation_feedback_events[] = { { "activation", "n", plasma_window_management_types + 16 }, }; WL_PRIVATE const struct wl_interface org_kde_plasma_activation_feedback_interface = { "org_kde_plasma_activation_feedback", 1, 1, org_kde_plasma_activation_feedback_requests, 1, org_kde_plasma_activation_feedback_events, }; static const struct wl_message org_kde_plasma_activation_requests[] = { { "destroy", "", plasma_window_management_types + 0 }, }; static const struct wl_message org_kde_plasma_activation_events[] = { { "app_id", "s", plasma_window_management_types + 0 }, { "finished", "", plasma_window_management_types + 0 }, }; WL_PRIVATE const struct wl_interface org_kde_plasma_activation_interface = { "org_kde_plasma_activation", 1, 1, org_kde_plasma_activation_requests, 2, org_kde_plasma_activation_events, }; dangvd-crystal-dock-7a34e96/src/display/plasma_window_management.h000066400000000000000000001134111512233520000253550ustar00rootroot00000000000000/* Generated by wayland-scanner 1.22.0 */ #ifndef PLASMA_WINDOW_MANAGEMENT_CLIENT_PROTOCOL_H #define PLASMA_WINDOW_MANAGEMENT_CLIENT_PROTOCOL_H #include #include #include "wayland-client.h" #ifdef __cplusplus extern "C" { #endif /** * @page page_plasma_window_management The plasma_window_management protocol * @section page_ifaces_plasma_window_management Interfaces * - @subpage page_iface_org_kde_plasma_window_management - application windows management * - @subpage page_iface_org_kde_plasma_window - interface to control application windows * - @subpage page_iface_org_kde_plasma_activation_feedback - activation feedback * - @subpage page_iface_org_kde_plasma_activation - * @section page_copyright_plasma_window_management Copyright *
 *
 * SPDX-FileCopyrightText: 2013-2014 Pier Luigi Fiorini
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 * 
*/ struct org_kde_plasma_activation; struct org_kde_plasma_activation_feedback; struct org_kde_plasma_window; struct org_kde_plasma_window_management; struct wl_output; struct wl_surface; #ifndef ORG_KDE_PLASMA_WINDOW_MANAGEMENT_INTERFACE #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_INTERFACE /** * @page page_iface_org_kde_plasma_window_management org_kde_plasma_window_management * @section page_iface_org_kde_plasma_window_management_desc Description * * This interface manages application windows. * It provides requests to show and hide the desktop and emits * an event every time a window is created so that the client can * use it to manage the window. * * Only one client can bind this interface at a time. * @section page_iface_org_kde_plasma_window_management_api API * See @ref iface_org_kde_plasma_window_management. */ /** * @defgroup iface_org_kde_plasma_window_management The org_kde_plasma_window_management interface * * This interface manages application windows. * It provides requests to show and hide the desktop and emits * an event every time a window is created so that the client can * use it to manage the window. * * Only one client can bind this interface at a time. */ extern const struct wl_interface org_kde_plasma_window_management_interface; #endif #ifndef ORG_KDE_PLASMA_WINDOW_INTERFACE #define ORG_KDE_PLASMA_WINDOW_INTERFACE /** * @page page_iface_org_kde_plasma_window org_kde_plasma_window * @section page_iface_org_kde_plasma_window_desc Description * * Manages and control an application window. * * Only one client can bind this interface at a time. * @section page_iface_org_kde_plasma_window_api API * See @ref iface_org_kde_plasma_window. */ /** * @defgroup iface_org_kde_plasma_window The org_kde_plasma_window interface * * Manages and control an application window. * * Only one client can bind this interface at a time. */ extern const struct wl_interface org_kde_plasma_window_interface; #endif #ifndef ORG_KDE_PLASMA_ACTIVATION_FEEDBACK_INTERFACE #define ORG_KDE_PLASMA_ACTIVATION_FEEDBACK_INTERFACE /** * @page page_iface_org_kde_plasma_activation_feedback org_kde_plasma_activation_feedback * @section page_iface_org_kde_plasma_activation_feedback_desc Description * * The activation manager interface provides a way to get notified * when an application is about to be activated. * @section page_iface_org_kde_plasma_activation_feedback_api API * See @ref iface_org_kde_plasma_activation_feedback. */ /** * @defgroup iface_org_kde_plasma_activation_feedback The org_kde_plasma_activation_feedback interface * * The activation manager interface provides a way to get notified * when an application is about to be activated. */ extern const struct wl_interface org_kde_plasma_activation_feedback_interface; #endif #ifndef ORG_KDE_PLASMA_ACTIVATION_INTERFACE #define ORG_KDE_PLASMA_ACTIVATION_INTERFACE /** * @page page_iface_org_kde_plasma_activation org_kde_plasma_activation * @section page_iface_org_kde_plasma_activation_api API * See @ref iface_org_kde_plasma_activation. */ /** * @defgroup iface_org_kde_plasma_activation The org_kde_plasma_activation interface */ extern const struct wl_interface org_kde_plasma_activation_interface; #endif #ifndef ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ENUM #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ENUM enum org_kde_plasma_window_management_state { ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ACTIVE = 0x1, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MINIMIZED = 0x2, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MAXIMIZED = 0x4, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_FULLSCREEN = 0x8, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_KEEP_ABOVE = 0x10, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_KEEP_BELOW = 0x20, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ON_ALL_DESKTOPS = 0x40, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_DEMANDS_ATTENTION = 0x80, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_CLOSEABLE = 0x100, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MINIMIZABLE = 0x200, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MAXIMIZABLE = 0x400, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_FULLSCREENABLE = 0x800, /** * @since 2 */ ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SKIPTASKBAR = 0x1000, /** * @since 3 */ ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SHADEABLE = 0x2000, /** * @since 3 */ ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SHADED = 0x4000, /** * @since 3 */ ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MOVABLE = 0x8000, /** * @since 3 */ ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_RESIZABLE = 0x10000, /** * @since 3 */ ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_VIRTUAL_DESKTOP_CHANGEABLE = 0x20000, /** * @since 9 */ ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SKIPSWITCHER = 0x40000, }; /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SKIPTASKBAR_SINCE_VERSION 2 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SHADEABLE_SINCE_VERSION 3 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SHADED_SINCE_VERSION 3 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MOVABLE_SINCE_VERSION 3 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_RESIZABLE_SINCE_VERSION 3 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_VIRTUAL_DESKTOP_CHANGEABLE_SINCE_VERSION 3 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SKIPSWITCHER_SINCE_VERSION 9 #endif /* ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ENUM */ #ifndef ORG_KDE_PLASMA_WINDOW_MANAGEMENT_SHOW_DESKTOP_ENUM #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_SHOW_DESKTOP_ENUM enum org_kde_plasma_window_management_show_desktop { ORG_KDE_PLASMA_WINDOW_MANAGEMENT_SHOW_DESKTOP_DISABLED = 0, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_SHOW_DESKTOP_ENABLED = 1, }; #endif /* ORG_KDE_PLASMA_WINDOW_MANAGEMENT_SHOW_DESKTOP_ENUM */ /** * @ingroup iface_org_kde_plasma_window_management * @struct org_kde_plasma_window_management_listener */ struct org_kde_plasma_window_management_listener { /** * notify the client when the show desktop mode is entered/left * * This event will be sent whenever the show desktop mode * changes. E.g. when it is entered or left. * * On binding the interface the current state is sent. * @param state new state */ void (*show_desktop_changed)(void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, uint32_t state); /** * notify the client that a window was mapped * * This event will be sent immediately after a window is mapped. * @param id Deprecated: internal window Id */ void (*window)(void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, uint32_t id); /** * notify the client when stacking order changed * * This event will be sent when stacking order changed and on * bind * @param ids internal windows id array * @since 11 */ void (*stacking_order_changed)(void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, struct wl_array *ids); /** * notify the client when stacking order changed * * This event will be sent when stacking order changed and on * bind * @param uuids internal windows id ;-separated * @since 12 */ void (*stacking_order_uuid_changed)(void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, const char *uuids); /** * notify the client that a window was mapped * * This event will be sent immediately after a window is mapped. * @param id Deprecated: internal window Id * @param uuid internal window uuid * @since 13 */ void (*window_with_uuid)(void *data, struct org_kde_plasma_window_management *org_kde_plasma_window_management, uint32_t id, const char *uuid); }; /** * @ingroup iface_org_kde_plasma_window_management */ static inline int org_kde_plasma_window_management_add_listener(struct org_kde_plasma_window_management *org_kde_plasma_window_management, const struct org_kde_plasma_window_management_listener *listener, void *data) { return wl_proxy_add_listener((struct wl_proxy *) org_kde_plasma_window_management, (void (**)(void)) listener, data); } #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_SHOW_DESKTOP 0 #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_GET_WINDOW 1 #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_GET_WINDOW_BY_UUID 2 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_SHOW_DESKTOP_CHANGED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_WINDOW_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STACKING_ORDER_CHANGED_SINCE_VERSION 11 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STACKING_ORDER_UUID_CHANGED_SINCE_VERSION 12 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_WINDOW_WITH_UUID_SINCE_VERSION 13 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_SHOW_DESKTOP_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_GET_WINDOW_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window_management */ #define ORG_KDE_PLASMA_WINDOW_MANAGEMENT_GET_WINDOW_BY_UUID_SINCE_VERSION 12 /** @ingroup iface_org_kde_plasma_window_management */ static inline void org_kde_plasma_window_management_set_user_data(struct org_kde_plasma_window_management *org_kde_plasma_window_management, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) org_kde_plasma_window_management, user_data); } /** @ingroup iface_org_kde_plasma_window_management */ static inline void * org_kde_plasma_window_management_get_user_data(struct org_kde_plasma_window_management *org_kde_plasma_window_management) { return wl_proxy_get_user_data((struct wl_proxy *) org_kde_plasma_window_management); } static inline uint32_t org_kde_plasma_window_management_get_version(struct org_kde_plasma_window_management *org_kde_plasma_window_management) { return wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window_management); } /** @ingroup iface_org_kde_plasma_window_management */ static inline void org_kde_plasma_window_management_destroy(struct org_kde_plasma_window_management *org_kde_plasma_window_management) { wl_proxy_destroy((struct wl_proxy *) org_kde_plasma_window_management); } /** * @ingroup iface_org_kde_plasma_window_management * * Tell the compositor to show/hide the desktop. */ static inline void org_kde_plasma_window_management_show_desktop(struct org_kde_plasma_window_management *org_kde_plasma_window_management, uint32_t state) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window_management, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_SHOW_DESKTOP, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window_management), 0, state); } /** * @ingroup iface_org_kde_plasma_window_management * Deprecated: use get_window_by_uuid */ static inline struct org_kde_plasma_window * org_kde_plasma_window_management_get_window(struct org_kde_plasma_window_management *org_kde_plasma_window_management, uint32_t internal_window_id) { struct wl_proxy *id; id = wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window_management, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_GET_WINDOW, &org_kde_plasma_window_interface, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window_management), 0, NULL, internal_window_id); return (struct org_kde_plasma_window *) id; } /** * @ingroup iface_org_kde_plasma_window_management */ static inline struct org_kde_plasma_window * org_kde_plasma_window_management_get_window_by_uuid(struct org_kde_plasma_window_management *org_kde_plasma_window_management, const char *internal_window_uuid) { struct wl_proxy *id; id = wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window_management, ORG_KDE_PLASMA_WINDOW_MANAGEMENT_GET_WINDOW_BY_UUID, &org_kde_plasma_window_interface, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window_management), 0, NULL, internal_window_uuid); return (struct org_kde_plasma_window *) id; } /** * @ingroup iface_org_kde_plasma_window * @struct org_kde_plasma_window_listener */ struct org_kde_plasma_window_listener { /** * window title has been changed * * This event will be sent as soon as the window title is * changed. * @param title window title */ void (*title_changed)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *title); /** * application identifier has been changed * * This event will be sent as soon as the application identifier * is changed. */ void (*app_id_changed)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *app_id); /** * window state has been changed * * This event will be sent as soon as the window state changes. * * Values for state argument are described by * org_kde_plasma_window_management.state. * @param flags bitfield of state flags */ void (*state_changed)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, uint32_t flags); /** * window was moved to another workspace * * DEPRECATED: use virtual_desktop_entered and * virtual_desktop_left instead This event will be sent when a * window is moved to another virtual desktop. * * It is not sent if it becomes visible on all virtual desktops * though. * @param number zero based virtual desktop number */ void (*virtual_desktop_changed)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, int32_t number); /** * window's icon name changed * * This event will be sent whenever the themed icon name changes. * May be null. * @param name the new themed icon name */ void (*themed_icon_name_changed)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *name); /** * window's surface was unmapped * * This event will be sent immediately after the window is closed * and its surface is unmapped. */ void (*unmapped)(void *data, struct org_kde_plasma_window *org_kde_plasma_window); /** * All initial known state is submitted * * This event will be sent immediately after all initial state * been sent to the client. If the Plasma window is already * unmapped, the unmapped event will be sent before the * initial_state event. * @since 4 */ void (*initial_state)(void *data, struct org_kde_plasma_window *org_kde_plasma_window); /** * The parent window changed * * This event will be sent whenever the parent window of this * org_kde_plasma_window changes. The passed parent is another * org_kde_plasma_window and this org_kde_plasma_window is a * transient window to the parent window. If the parent argument is * null, this org_kde_plasma_window does not have a parent window. * @param parent The parent window * @since 5 */ void (*parent_window)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, struct org_kde_plasma_window *parent); /** * The geometry of this window in absolute coordinates * * This event will be sent whenever the window geometry of this * org_kde_plasma_window changes. The coordinates are in absolute * coordinates of the windowing system. * @param x x position of the org_kde_plasma_window * @param y y position of the org_kde_plasma_window * @param width width of the org_kde_plasma_window * @param height height of the org_kde_plasma_window * @since 6 */ void (*geometry)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, int32_t x, int32_t y, uint32_t width, uint32_t height); /** * The icon of the window changed * * This event will be sent whenever the icon of the window * changes, but there is no themed icon name. Common examples are * Xwayland windows which have a pixmap based icon. * * The client can request the icon using get_icon. * @since 7 */ void (*icon_changed)(void *data, struct org_kde_plasma_window *org_kde_plasma_window); /** * process id of application owning the window has changed * * This event will be sent when the compositor has set the * process id this window belongs to. This should be set once * before the initial_state is sent. * @param pid process id */ void (*pid_changed)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, uint32_t pid); /** * the window entered a new virtual desktop * * This event will be sent when the window has entered a new * virtual desktop. The window can be on more than one desktop, or * none: then is considered on all of them. * @param id desktop id * @since 8 */ void (*virtual_desktop_entered)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *id); /** * the window left a virtual desktop * * This event will be sent when the window left a virtual * desktop. If the window leaves all desktops, it can be considered * on all. If the window gets manually added on all desktops, the * server has to send virtual_desktop_left for every previous * desktop it was in for the window to be really considered on all * desktops. * @param is desktop id * @since 8 */ void (*virtual_desktop_left)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *is); /** * notify the client that the current appmenu changed * * This event will be sent after the application menu for the * window has changed. * @since 10 */ void (*application_menu)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *service_name, const char *object_path); /** * the window entered an activity * * This event will be sent when the window has entered an * activity. The window can be on more than one activity, or none: * then is considered on all of them. * @param id activity id * @since 14 */ void (*activity_entered)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *id); /** * the window left an activity * * This event will be sent when the window left an activity. If * the window leaves all activities, it will be considered on all. * If the window gets manually added on all activities, the server * has to send activity_left for every previous activity it was in * for the window to be really considered on all activities. * @param id activity id * @since 14 */ void (*activity_left)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *id); /** * X11 resource name has changed * * This event will be sent when the X11 resource name of the * window has changed. This is only set for XWayland windows. * @param resource_name resource name * @since 16 */ void (*resource_name_changed)(void *data, struct org_kde_plasma_window *org_kde_plasma_window, const char *resource_name); }; /** * @ingroup iface_org_kde_plasma_window */ static inline int org_kde_plasma_window_add_listener(struct org_kde_plasma_window *org_kde_plasma_window, const struct org_kde_plasma_window_listener *listener, void *data) { return wl_proxy_add_listener((struct wl_proxy *) org_kde_plasma_window, (void (**)(void)) listener, data); } #define ORG_KDE_PLASMA_WINDOW_SET_STATE 0 #define ORG_KDE_PLASMA_WINDOW_SET_VIRTUAL_DESKTOP 1 #define ORG_KDE_PLASMA_WINDOW_SET_MINIMIZED_GEOMETRY 2 #define ORG_KDE_PLASMA_WINDOW_UNSET_MINIMIZED_GEOMETRY 3 #define ORG_KDE_PLASMA_WINDOW_CLOSE 4 #define ORG_KDE_PLASMA_WINDOW_REQUEST_MOVE 5 #define ORG_KDE_PLASMA_WINDOW_REQUEST_RESIZE 6 #define ORG_KDE_PLASMA_WINDOW_DESTROY 7 #define ORG_KDE_PLASMA_WINDOW_GET_ICON 8 #define ORG_KDE_PLASMA_WINDOW_REQUEST_ENTER_VIRTUAL_DESKTOP 9 #define ORG_KDE_PLASMA_WINDOW_REQUEST_ENTER_NEW_VIRTUAL_DESKTOP 10 #define ORG_KDE_PLASMA_WINDOW_REQUEST_LEAVE_VIRTUAL_DESKTOP 11 #define ORG_KDE_PLASMA_WINDOW_REQUEST_ENTER_ACTIVITY 12 #define ORG_KDE_PLASMA_WINDOW_REQUEST_LEAVE_ACTIVITY 13 #define ORG_KDE_PLASMA_WINDOW_SEND_TO_OUTPUT 14 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_TITLE_CHANGED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_APP_ID_CHANGED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_STATE_CHANGED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_VIRTUAL_DESKTOP_CHANGED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_THEMED_ICON_NAME_CHANGED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_UNMAPPED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_INITIAL_STATE_SINCE_VERSION 4 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_PARENT_WINDOW_SINCE_VERSION 5 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_GEOMETRY_SINCE_VERSION 6 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_ICON_CHANGED_SINCE_VERSION 7 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_PID_CHANGED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_VIRTUAL_DESKTOP_ENTERED_SINCE_VERSION 8 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_VIRTUAL_DESKTOP_LEFT_SINCE_VERSION 8 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_APPLICATION_MENU_SINCE_VERSION 10 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_ACTIVITY_ENTERED_SINCE_VERSION 14 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_ACTIVITY_LEFT_SINCE_VERSION 14 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_RESOURCE_NAME_CHANGED_SINCE_VERSION 16 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_SET_STATE_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_SET_VIRTUAL_DESKTOP_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_SET_MINIMIZED_GEOMETRY_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_UNSET_MINIMIZED_GEOMETRY_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_CLOSE_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_REQUEST_MOVE_SINCE_VERSION 3 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_REQUEST_RESIZE_SINCE_VERSION 3 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_DESTROY_SINCE_VERSION 4 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_GET_ICON_SINCE_VERSION 7 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_REQUEST_ENTER_VIRTUAL_DESKTOP_SINCE_VERSION 8 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_REQUEST_ENTER_NEW_VIRTUAL_DESKTOP_SINCE_VERSION 8 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_REQUEST_LEAVE_VIRTUAL_DESKTOP_SINCE_VERSION 8 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_REQUEST_ENTER_ACTIVITY_SINCE_VERSION 14 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_REQUEST_LEAVE_ACTIVITY_SINCE_VERSION 14 /** * @ingroup iface_org_kde_plasma_window */ #define ORG_KDE_PLASMA_WINDOW_SEND_TO_OUTPUT_SINCE_VERSION 15 /** @ingroup iface_org_kde_plasma_window */ static inline void org_kde_plasma_window_set_user_data(struct org_kde_plasma_window *org_kde_plasma_window, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) org_kde_plasma_window, user_data); } /** @ingroup iface_org_kde_plasma_window */ static inline void * org_kde_plasma_window_get_user_data(struct org_kde_plasma_window *org_kde_plasma_window) { return wl_proxy_get_user_data((struct wl_proxy *) org_kde_plasma_window); } static inline uint32_t org_kde_plasma_window_get_version(struct org_kde_plasma_window *org_kde_plasma_window) { return wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window); } /** * @ingroup iface_org_kde_plasma_window * * Set window state. * * Values for state argument are described by org_kde_plasma_window_management.state * and can be used together in a bitfield. The flags bitfield describes which flags are * supposed to be set, the state bitfield the value for the set flags */ static inline void org_kde_plasma_window_set_state(struct org_kde_plasma_window *org_kde_plasma_window, uint32_t flags, uint32_t state) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_SET_STATE, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0, flags, state); } /** * @ingroup iface_org_kde_plasma_window * * Deprecated: use enter_virtual_desktop * Maps the window to a different virtual desktop. * * To show the window on all virtual desktops, call the * org_kde_plasma_window.set_state request and specify a on_all_desktops * state in the bitfield. */ static inline void org_kde_plasma_window_set_virtual_desktop(struct org_kde_plasma_window *org_kde_plasma_window, uint32_t number) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_SET_VIRTUAL_DESKTOP, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0, number); } /** * @ingroup iface_org_kde_plasma_window * * Sets the geometry of the taskbar entry for this window. * The geometry is relative to a panel in particular. */ static inline void org_kde_plasma_window_set_minimized_geometry(struct org_kde_plasma_window *org_kde_plasma_window, struct wl_surface *panel, uint32_t x, uint32_t y, uint32_t width, uint32_t height) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_SET_MINIMIZED_GEOMETRY, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0, panel, x, y, width, height); } /** * @ingroup iface_org_kde_plasma_window * * Remove the task geometry information for a particular panel. */ static inline void org_kde_plasma_window_unset_minimized_geometry(struct org_kde_plasma_window *org_kde_plasma_window, struct wl_surface *panel) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_UNSET_MINIMIZED_GEOMETRY, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0, panel); } /** * @ingroup iface_org_kde_plasma_window * * Close this window. */ static inline void org_kde_plasma_window_close(struct org_kde_plasma_window *org_kde_plasma_window) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_CLOSE, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0); } /** * @ingroup iface_org_kde_plasma_window * * Request an interactive move for this window. */ static inline void org_kde_plasma_window_request_move(struct org_kde_plasma_window *org_kde_plasma_window) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_REQUEST_MOVE, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0); } /** * @ingroup iface_org_kde_plasma_window * * Request an interactive resize for this window. */ static inline void org_kde_plasma_window_request_resize(struct org_kde_plasma_window *org_kde_plasma_window) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_REQUEST_RESIZE, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0); } /** * @ingroup iface_org_kde_plasma_window * * Removes the resource bound for this org_kde_plasma_window. */ static inline void org_kde_plasma_window_destroy(struct org_kde_plasma_window *org_kde_plasma_window) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), WL_MARSHAL_FLAG_DESTROY); } /** * @ingroup iface_org_kde_plasma_window * * The compositor will write the window icon into the provided file descriptor. * The data is a serialized QIcon with QDataStream. */ static inline void org_kde_plasma_window_get_icon(struct org_kde_plasma_window *org_kde_plasma_window, int32_t fd) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_GET_ICON, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0, fd); } /** * @ingroup iface_org_kde_plasma_window * * Make the window enter a virtual desktop. A window can enter more * than one virtual desktop. if the id is empty or invalid, no action will be performed. */ static inline void org_kde_plasma_window_request_enter_virtual_desktop(struct org_kde_plasma_window *org_kde_plasma_window, const char *id) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_REQUEST_ENTER_VIRTUAL_DESKTOP, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0, id); } /** * @ingroup iface_org_kde_plasma_window * RFC: do this with an empty id to request_enter_virtual_desktop? * Make the window enter a new virtual desktop. If the server consents the request, * it will create a new virtual desktop and assign the window to it. */ static inline void org_kde_plasma_window_request_enter_new_virtual_desktop(struct org_kde_plasma_window *org_kde_plasma_window) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_REQUEST_ENTER_NEW_VIRTUAL_DESKTOP, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0); } /** * @ingroup iface_org_kde_plasma_window * * Make the window exit a virtual desktop. If it exits all desktops it will be considered on all of them. */ static inline void org_kde_plasma_window_request_leave_virtual_desktop(struct org_kde_plasma_window *org_kde_plasma_window, const char *id) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_REQUEST_LEAVE_VIRTUAL_DESKTOP, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0, id); } /** * @ingroup iface_org_kde_plasma_window * * Make the window enter an activity. A window can enter more activity. If the id is empty or invalid, no action will be performed. */ static inline void org_kde_plasma_window_request_enter_activity(struct org_kde_plasma_window *org_kde_plasma_window, const char *id) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_REQUEST_ENTER_ACTIVITY, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0, id); } /** * @ingroup iface_org_kde_plasma_window * * Make the window exit a an activity. If it exits all activities it will be considered on all of them. */ static inline void org_kde_plasma_window_request_leave_activity(struct org_kde_plasma_window *org_kde_plasma_window, const char *id) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_REQUEST_LEAVE_ACTIVITY, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0, id); } /** * @ingroup iface_org_kde_plasma_window * * Requests this window to be displayed in a specific output. */ static inline void org_kde_plasma_window_send_to_output(struct org_kde_plasma_window *org_kde_plasma_window, struct wl_output *output) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_window, ORG_KDE_PLASMA_WINDOW_SEND_TO_OUTPUT, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_window), 0, output); } /** * @ingroup iface_org_kde_plasma_activation_feedback * @struct org_kde_plasma_activation_feedback_listener */ struct org_kde_plasma_activation_feedback_listener { /** * notify that an app is starting * * Will be issued when an app is set to be activated. It offers * an instance of org_kde_plasma_activation that will tell us the * app_id and the extent of the activation. */ void (*activation)(void *data, struct org_kde_plasma_activation_feedback *org_kde_plasma_activation_feedback, struct org_kde_plasma_activation *id); }; /** * @ingroup iface_org_kde_plasma_activation_feedback */ static inline int org_kde_plasma_activation_feedback_add_listener(struct org_kde_plasma_activation_feedback *org_kde_plasma_activation_feedback, const struct org_kde_plasma_activation_feedback_listener *listener, void *data) { return wl_proxy_add_listener((struct wl_proxy *) org_kde_plasma_activation_feedback, (void (**)(void)) listener, data); } #define ORG_KDE_PLASMA_ACTIVATION_FEEDBACK_DESTROY 0 /** * @ingroup iface_org_kde_plasma_activation_feedback */ #define ORG_KDE_PLASMA_ACTIVATION_FEEDBACK_ACTIVATION_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_activation_feedback */ #define ORG_KDE_PLASMA_ACTIVATION_FEEDBACK_DESTROY_SINCE_VERSION 1 /** @ingroup iface_org_kde_plasma_activation_feedback */ static inline void org_kde_plasma_activation_feedback_set_user_data(struct org_kde_plasma_activation_feedback *org_kde_plasma_activation_feedback, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) org_kde_plasma_activation_feedback, user_data); } /** @ingroup iface_org_kde_plasma_activation_feedback */ static inline void * org_kde_plasma_activation_feedback_get_user_data(struct org_kde_plasma_activation_feedback *org_kde_plasma_activation_feedback) { return wl_proxy_get_user_data((struct wl_proxy *) org_kde_plasma_activation_feedback); } static inline uint32_t org_kde_plasma_activation_feedback_get_version(struct org_kde_plasma_activation_feedback *org_kde_plasma_activation_feedback) { return wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_activation_feedback); } /** * @ingroup iface_org_kde_plasma_activation_feedback * * Destroy the activation manager object. The activation objects introduced * by this manager object will be unaffected. */ static inline void org_kde_plasma_activation_feedback_destroy(struct org_kde_plasma_activation_feedback *org_kde_plasma_activation_feedback) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_activation_feedback, ORG_KDE_PLASMA_ACTIVATION_FEEDBACK_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_activation_feedback), WL_MARSHAL_FLAG_DESTROY); } /** * @ingroup iface_org_kde_plasma_activation * @struct org_kde_plasma_activation_listener */ struct org_kde_plasma_activation_listener { /** * Offers the app_id * * * @param app_id application id, as described in xdg_activation_v1 */ void (*app_id)(void *data, struct org_kde_plasma_activation *org_kde_plasma_activation, const char *app_id); /** * Notifies about activation finished, either by activation or because it got invalidated * * */ void (*finished)(void *data, struct org_kde_plasma_activation *org_kde_plasma_activation); }; /** * @ingroup iface_org_kde_plasma_activation */ static inline int org_kde_plasma_activation_add_listener(struct org_kde_plasma_activation *org_kde_plasma_activation, const struct org_kde_plasma_activation_listener *listener, void *data) { return wl_proxy_add_listener((struct wl_proxy *) org_kde_plasma_activation, (void (**)(void)) listener, data); } #define ORG_KDE_PLASMA_ACTIVATION_DESTROY 0 /** * @ingroup iface_org_kde_plasma_activation */ #define ORG_KDE_PLASMA_ACTIVATION_APP_ID_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_activation */ #define ORG_KDE_PLASMA_ACTIVATION_FINISHED_SINCE_VERSION 1 /** * @ingroup iface_org_kde_plasma_activation */ #define ORG_KDE_PLASMA_ACTIVATION_DESTROY_SINCE_VERSION 1 /** @ingroup iface_org_kde_plasma_activation */ static inline void org_kde_plasma_activation_set_user_data(struct org_kde_plasma_activation *org_kde_plasma_activation, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) org_kde_plasma_activation, user_data); } /** @ingroup iface_org_kde_plasma_activation */ static inline void * org_kde_plasma_activation_get_user_data(struct org_kde_plasma_activation *org_kde_plasma_activation) { return wl_proxy_get_user_data((struct wl_proxy *) org_kde_plasma_activation); } static inline uint32_t org_kde_plasma_activation_get_version(struct org_kde_plasma_activation *org_kde_plasma_activation) { return wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_activation); } /** * @ingroup iface_org_kde_plasma_activation * * Notify the compositor that the org_kde_plasma_activation object will no * longer be used. */ static inline void org_kde_plasma_activation_destroy(struct org_kde_plasma_activation *org_kde_plasma_activation) { wl_proxy_marshal_flags((struct wl_proxy *) org_kde_plasma_activation, ORG_KDE_PLASMA_ACTIVATION_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) org_kde_plasma_activation), WL_MARSHAL_FLAG_DESTROY); } #ifdef __cplusplus } #endif #endif dangvd-crystal-dock-7a34e96/src/display/window_system.cc000066400000000000000000000156611512233520000233760ustar00rootroot00000000000000#include "window_system.h" #include #include #include #include #include #include #include #include "kde_auto_hide_manager.h" #include "kde_virtual_desktop_manager.h" #include "kde_window_manager.h" #include "wlr_window_manager.h" namespace crystaldock { namespace { LayerShellQt::Window* getLayerShellWin(QWidget* widget) { QWindow* win = WindowSystem::getWindow(widget); if (win == nullptr) { std::cerr << "Null QWindow" << std::endl; return nullptr; } return LayerShellQt::Window::get(win); } } // namespace org_kde_plasma_virtual_desktop_management* WindowSystem::kde_virtual_desktop_management_; org_kde_plasma_window_management* WindowSystem::kde_window_management_; kde_screen_edge_manager_v1* WindowSystem::kde_screen_edge_manager_; zwlr_foreign_toplevel_manager_v1* WindowSystem::wlr_window_manager_; VirtualDesktopManager WindowSystem::virtualDesktopManager_; WindowManager WindowSystem::windowManager_; AutoHideManager WindowSystem::autoHideManager_; std::vector WindowSystem::screens_; std::unique_ptr WindowSystem::activityManager_; /* static */ WindowSystem* WindowSystem::self() { static WindowSystem self; return &self; } /* static */ bool WindowSystem::init(struct wl_display* display) { struct wl_registry *registry = wl_display_get_registry(display); wl_registry_add_listener(registry, ®istry_listener_, NULL); // wait for the "initial" set of globals to appear wl_display_roundtrip(display); if (!kde_window_management_ && !wlr_window_manager_) { std::cerr << "Failed to bind required Wayland interfaces" << std::endl; return false; } if (kde_virtual_desktop_management_) { KdeVirtualDesktopManager::init(kde_virtual_desktop_management_); KdeVirtualDesktopManager::bindVirtualDesktopManagerFunctions(&virtualDesktopManager_); } if (kde_window_management_) { KdeWindowManager::init(kde_window_management_); KdeWindowManager::bindWindowManagerFunctions(&windowManager_); } else if (wlr_window_manager_) { WlrWindowManager::init(wlr_window_manager_); WlrWindowManager::bindWindowManagerFunctions(&windowManager_); } if (kde_screen_edge_manager_) { KdeAutoHideManager::init(kde_screen_edge_manager_); KdeAutoHideManager::bindAutoHideManagerFunctions(&autoHideManager_); } LayerShellQt::Shell::useLayerShell(); activityManager_ = std::make_unique( "org.kde.ActivityManager", "/ActivityManager/Activities", "org.kde.ActivityManager.Activities"); if (activityManager_->isValid()) { const QDBusReply reply = activityManager_->call("CurrentActivity"); if (reply.isValid()) { WindowSystem::self()->setCurrentActivity(reply.value().toStdString()); } connect(activityManager_.get(), SIGNAL(CurrentActivityChanged(QString)), WindowSystem::self(), SLOT(onCurrentActivityChanged(QString))); } initScreens(); return true; } /* static */ bool WindowSystem::hasVirtualDesktopManager() { return kde_virtual_desktop_management_ != nullptr; } /* static */ bool WindowSystem::hasAutoHideManager() { return kde_screen_edge_manager_ != nullptr; } /* static */ bool WindowSystem::hasActivityManager() { return activityManager_->isValid(); } /* static */ void WindowSystem::setAnchorAndStrut( QWidget* widget, LayerShellQt::Window::Anchors anchors, uint32_t strutSize) { auto* layerShellWin = getLayerShellWin(widget); if (layerShellWin) { layerShellWin->setAnchors(anchors); layerShellWin->setExclusiveZone(strutSize); layerShellWin->setScreenConfiguration(LayerShellQt::Window::ScreenFromQWindow); } } /*static*/ void WindowSystem::setLayer(QWidget* widget, LayerShellQt::Window::Layer layer) { auto* layerShellWin = getLayerShellWin(widget); if (layerShellWin) { layerShellWin->setLayer(layer); } } void WindowSystem::initScreens() { screens_.clear(); for (auto* screen : QGuiApplication::screens()) { screens_.push_back(screen); } std::sort(screens_.begin(), screens_.end(), [](QScreen* s1, QScreen* s2) { return s1->geometry().center().manhattanLength() < s2->geometry().center().manhattanLength(); }); } /*static*/ std::vector WindowSystem::screens() { return screens_; } /*static*/ void WindowSystem::setScreen(QWidget* widget, int screen) { if (screen < 0 || screen >= static_cast(screens_.size())) { return; } QWindow* win = getWindow(widget); if (win) { win->setScreen(screens_[screen]); } } /*static*/ wl_output* WindowSystem::getWlOutputForScreen(int screen) { if (screen < 0 || screen >= static_cast(screens_.size())) { return nullptr; } auto* waylandScreen = screens_[screen]->nativeInterface(); if (waylandScreen) { return waylandScreen->output(); } return nullptr; } // wl_registry interface. /* static */ void WindowSystem::registry_global( void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { if (std::string(interface) == "org_kde_plasma_virtual_desktop_management") { kde_virtual_desktop_management_ = reinterpret_cast(wl_registry_bind( registry, name, &org_kde_plasma_virtual_desktop_management_interface, 2)); if (!kde_virtual_desktop_management_) { std::cerr << "Failed to bind org_kde_plasma_virtual_desktop_management Wayland interface" << std::endl; } } else if (std::string(interface) == "org_kde_plasma_window_management") { kde_window_management_ = reinterpret_cast(wl_registry_bind( registry, name, &org_kde_plasma_window_management_interface, 16)); if (!kde_window_management_) { std::cerr << "Failed to bind org_kde_plasma_window_management Wayland interface. " << "Maybe another client has already bound it?" << std::endl; } } else if (std::string(interface) == "kde_screen_edge_manager_v1") { kde_screen_edge_manager_ = reinterpret_cast(wl_registry_bind( registry, name, &kde_screen_edge_manager_v1_interface, 1)); if (!kde_screen_edge_manager_) { std::cerr << "Failed to bind kde_screen_edge_manager_v1 Wayland interface" << std::endl; } } else if (std::string(interface) == "zwlr_foreign_toplevel_manager_v1") { wlr_window_manager_ = reinterpret_cast(wl_registry_bind( registry, name, &zwlr_foreign_toplevel_manager_v1_interface, 3)); if (!wlr_window_manager_) { std::cerr << "Failed to bind zwlr_foreign_toplevel_manager_v1 Wayland interface" << std::endl; } } } /* static */ void WindowSystem::registry_global_remove( void *data, struct wl_registry *wl_registry, uint32_t name) { // Ignore. } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/display/window_system.h000066400000000000000000000202601512233520000232270ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2023 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_WINDOW_SYSTEM_H_ #define CRYSTALDOCK_WINDOW_SYSTEM_H_ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kde_screen_edge.h" #include "plasma_virtual_desktop.h" #include "plasma_window_management.h" #include "wlr_foreign_toplevel_management.h" #include namespace crystaldock { struct VirtualDesktopInfo { std::string id; uint32_t number; // 1-based. std::string name; // Pointer to an implementation-specific virtual desktop struct. void* virtual_desktop; }; struct WindowInfo { // Pointer to an implementation-specific windows struct. void* window; std::string appId; std::string title; std::string icon; std::string desktop; std::string activity; bool initialized; bool skipTaskbar; bool onAllDesktops; bool demandsAttention; bool minimized; bool maximized; bool fullscreen; bool restoreAfterShowDesktop; int32_t x; int32_t y; uint32_t width; uint32_t height; uint32_t mapping_order; std::unordered_set outputs; }; struct VirtualDesktopManager { int (*numberOfDesktops)(); std::vector (*desktops)(); std::string_view (*currentDesktop)(); void (*setCurrentDesktop)(std::string_view); }; struct WindowManager { std::vector (*windows)(); void* (*activeWindow)(); // We manually reset active window, usually when the new active window is the dock itself. // We don't want to always do this (e.g. handle this in state_change() handler) because // otherwise we wouldn't be able to click on an active window's icon to minimize it // (the click action would change the active window to be the dock). void (*resetActiveWindow)(); void (*activateWindow)(void* window); void (*activateOrMinimizeWindow)(void* window); void (*minimizeWindow)(void* window); void (*closeWindow)(void* window); bool (*showingDesktop)(); void (*setShowingDesktop)(bool show); }; struct AutoHideManager { void (*setAutoHide)(QWidget* widget, Qt::Edge edge, bool on); }; class WindowSystem : public QObject { Q_OBJECT private: WindowSystem() = default; signals: void currentDesktopChanged(std::string_view); void numberOfDesktopsChanged(int); void desktopNameChanged(std::string_view desktopId, std::string_view desktopName); void windowAdded(const WindowInfo*); void windowRemoved(void*); void windowLeftCurrentDesktop(void*); void windowGeometryChanged(const WindowInfo*); void windowStateChanged(const WindowInfo*); void windowTitleChanged(const WindowInfo*); void activeWindowChanged(void*); void windowLeftCurrentActivity(void*); // Signals emitted when a window entered or left an output (screen). void windowEnteredOutput(const WindowInfo*, const wl_output*); void windowLeftOutput(const WindowInfo*, const wl_output*); void currentActivityChanged(std::string_view); private: // We need this to be non-static for the slot. std::string currentActivity_; public: static std::string_view currentActivity() { return WindowSystem::self()->currentActivity_; } void setCurrentActivity(std::string_view activity) { currentActivity_ = activity; } public slots: void onCurrentActivityChanged(QString activity) { currentActivity_ = activity.toStdString(); emit currentActivityChanged(activity.toStdString()); } public: static WindowSystem* self(); static bool init(struct wl_display* display); static bool hasVirtualDesktopManager(); static bool hasAutoHideManager(); static bool hasActivityManager(); static int numberOfDesktops() { if (hasVirtualDesktopManager()) { return virtualDesktopManager_.numberOfDesktops(); } return 1; } static std::vector desktops() { if (hasVirtualDesktopManager()) { return virtualDesktopManager_.desktops(); } return {}; } static std::string_view currentDesktop() { if (hasVirtualDesktopManager()) { return virtualDesktopManager_.currentDesktop(); } static constexpr char kDesktop[] = ""; return kDesktop; } static void setCurrentDesktop(std::string_view desktop) { if (hasVirtualDesktopManager()) { virtualDesktopManager_.setCurrentDesktop(desktop); } } static std::vector windows() { return windowManager_.windows(); } static void* activeWindow() { return windowManager_.activeWindow(); } // We manually reset active window, usually when the new active window is the dock itself. // We don't want to always do this (e.g. handle this in state_change() handler) because // otherwise we wouldn't be able to click on an active window's icon to minimize it // (the click action would change the active window to be the dock). static void resetActiveWindow() { windowManager_.resetActiveWindow(); } static void activateWindow(void* window) { windowManager_.activateWindow(window); } static void activateOrMinimizeWindow(void* window) { windowManager_.activateOrMinimizeWindow(window); } static void minimizeWindow(void* window) { windowManager_.minimizeWindow(window); } static void closeWindow(void* window) { windowManager_.closeWindow(window); } static bool showingDesktop() { return windowManager_.showingDesktop(); } static void setShowingDesktop(bool show) { windowManager_.setShowingDesktop(show); } static void setAutoHide(QWidget* widget, Qt::Edge edge, bool on = true) { if (hasAutoHideManager()) { autoHideManager_.setAutoHide(widget, edge, on); } } static void setAnchorAndStrut(QWidget* widget, LayerShellQt::Window::Anchors anchors, uint32_t strutSize); static void setLayer(QWidget* widget, LayerShellQt::Window::Layer layer); static std::vector screens(); // Sets the widget to display on the screen with index `screen` (0-based). static void setScreen(QWidget* widget, int screen); // Return wl_output object for the screen with index `screen` (0-based). static wl_output* getWlOutputForScreen(int screen); static QWindow* getWindow(QWidget* widget) { widget->winId(); // we need this for widget->windowHandle() to not return nullptr. return widget->windowHandle(); } private: // wl_registry interface. static void registry_global( void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version ); static void registry_global_remove(void *data, struct wl_registry *wl_registry, uint32_t name); static constexpr struct wl_registry_listener registry_listener_ = { registry_global, registry_global_remove }; static void initScreens(); static org_kde_plasma_virtual_desktop_management* kde_virtual_desktop_management_; static org_kde_plasma_window_management* kde_window_management_; static kde_screen_edge_manager_v1* kde_screen_edge_manager_; static zwlr_foreign_toplevel_manager_v1* wlr_window_manager_; static VirtualDesktopManager virtualDesktopManager_; static WindowManager windowManager_; static AutoHideManager autoHideManager_; static std::vector screens_; static std::unique_ptr activityManager_; }; } // namespace crystaldock #endif // CRYSTALDOCK_WINDOW_SYSTEM_H_ dangvd-crystal-dock-7a34e96/src/display/wlr_foreign_toplevel_management.c000066400000000000000000000107451512233520000267410ustar00rootroot00000000000000/* Generated by wayland-scanner 1.23.1 */ /* * Copyright © 2018 Ilia Bozhinov * * Permission to use, copy, modify, distribute, and sell this * software and its documentation for any purpose is hereby granted * without fee, provided that the above copyright notice appear in * all copies and that both that copyright notice and this permission * notice appear in supporting documentation, and that the name of * the copyright holders not be used in advertising or publicity * pertaining to distribution of the software without specific, * written prior permission. The copyright holders make no * representations about the suitability of this software for any * purpose. It is provided "as is" without express or implied * warranty. * * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF * THIS SOFTWARE. */ #include #include #include #include "wayland-util.h" #ifndef __has_attribute # define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) #define WL_PRIVATE __attribute__ ((visibility("hidden"))) #else #define WL_PRIVATE #endif extern const struct wl_interface wl_output_interface; extern const struct wl_interface wl_seat_interface; extern const struct wl_interface wl_surface_interface; extern const struct wl_interface zwlr_foreign_toplevel_handle_v1_interface; static const struct wl_interface *wlr_foreign_toplevel_management_unstable_v1_types[] = { NULL, &zwlr_foreign_toplevel_handle_v1_interface, &wl_seat_interface, &wl_surface_interface, NULL, NULL, NULL, NULL, &wl_output_interface, &wl_output_interface, &wl_output_interface, &zwlr_foreign_toplevel_handle_v1_interface, }; static const struct wl_message zwlr_foreign_toplevel_manager_v1_requests[] = { { "stop", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, }; static const struct wl_message zwlr_foreign_toplevel_manager_v1_events[] = { { "toplevel", "n", wlr_foreign_toplevel_management_unstable_v1_types + 1 }, { "finished", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, }; WL_PRIVATE const struct wl_interface zwlr_foreign_toplevel_manager_v1_interface = { "zwlr_foreign_toplevel_manager_v1", 3, 1, zwlr_foreign_toplevel_manager_v1_requests, 2, zwlr_foreign_toplevel_manager_v1_events, }; static const struct wl_message zwlr_foreign_toplevel_handle_v1_requests[] = { { "set_maximized", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, { "unset_maximized", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, { "set_minimized", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, { "unset_minimized", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, { "activate", "o", wlr_foreign_toplevel_management_unstable_v1_types + 2 }, { "close", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, { "set_rectangle", "oiiii", wlr_foreign_toplevel_management_unstable_v1_types + 3 }, { "destroy", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, { "set_fullscreen", "2?o", wlr_foreign_toplevel_management_unstable_v1_types + 8 }, { "unset_fullscreen", "2", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, }; static const struct wl_message zwlr_foreign_toplevel_handle_v1_events[] = { { "title", "s", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, { "app_id", "s", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, { "output_enter", "o", wlr_foreign_toplevel_management_unstable_v1_types + 9 }, { "output_leave", "o", wlr_foreign_toplevel_management_unstable_v1_types + 10 }, { "state", "a", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, { "done", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, { "closed", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, { "parent", "3?o", wlr_foreign_toplevel_management_unstable_v1_types + 11 }, }; WL_PRIVATE const struct wl_interface zwlr_foreign_toplevel_handle_v1_interface = { "zwlr_foreign_toplevel_handle_v1", 3, 10, zwlr_foreign_toplevel_handle_v1_requests, 8, zwlr_foreign_toplevel_handle_v1_events, }; dangvd-crystal-dock-7a34e96/src/display/wlr_foreign_toplevel_management.h000066400000000000000000000553701512233520000267510ustar00rootroot00000000000000/* Generated by wayland-scanner 1.23.1 */ #ifndef WLR_FOREIGN_TOPLEVEL_MANAGEMENT_UNSTABLE_V1_CLIENT_PROTOCOL_H #define WLR_FOREIGN_TOPLEVEL_MANAGEMENT_UNSTABLE_V1_CLIENT_PROTOCOL_H #include #include #include "wayland-client.h" #ifdef __cplusplus extern "C" { #endif /** * @page page_wlr_foreign_toplevel_management_unstable_v1 The wlr_foreign_toplevel_management_unstable_v1 protocol * @section page_ifaces_wlr_foreign_toplevel_management_unstable_v1 Interfaces * - @subpage page_iface_zwlr_foreign_toplevel_manager_v1 - list and control opened apps * - @subpage page_iface_zwlr_foreign_toplevel_handle_v1 - an opened toplevel * @section page_copyright_wlr_foreign_toplevel_management_unstable_v1 Copyright *
 *
 * Copyright © 2018 Ilia Bozhinov
 *
 * Permission to use, copy, modify, distribute, and sell this
 * software and its documentation for any purpose is hereby granted
 * without fee, provided that the above copyright notice appear in
 * all copies and that both that copyright notice and this permission
 * notice appear in supporting documentation, and that the name of
 * the copyright holders not be used in advertising or publicity
 * pertaining to distribution of the software without specific,
 * written prior permission.  The copyright holders make no
 * representations about the suitability of this software for any
 * purpose.  It is provided "as is" without express or implied
 * warranty.
 *
 * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
 * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
 * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
 * THIS SOFTWARE.
 * 
*/ struct wl_output; struct wl_seat; struct wl_surface; struct zwlr_foreign_toplevel_handle_v1; struct zwlr_foreign_toplevel_manager_v1; #ifndef ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_INTERFACE #define ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_INTERFACE /** * @page page_iface_zwlr_foreign_toplevel_manager_v1 zwlr_foreign_toplevel_manager_v1 * @section page_iface_zwlr_foreign_toplevel_manager_v1_desc Description * * The purpose of this protocol is to enable the creation of taskbars * and docks by providing them with a list of opened applications and * letting them request certain actions on them, like maximizing, etc. * * After a client binds the zwlr_foreign_toplevel_manager_v1, each opened * toplevel window will be sent via the toplevel event * @section page_iface_zwlr_foreign_toplevel_manager_v1_api API * See @ref iface_zwlr_foreign_toplevel_manager_v1. */ /** * @defgroup iface_zwlr_foreign_toplevel_manager_v1 The zwlr_foreign_toplevel_manager_v1 interface * * The purpose of this protocol is to enable the creation of taskbars * and docks by providing them with a list of opened applications and * letting them request certain actions on them, like maximizing, etc. * * After a client binds the zwlr_foreign_toplevel_manager_v1, each opened * toplevel window will be sent via the toplevel event */ extern const struct wl_interface zwlr_foreign_toplevel_manager_v1_interface; #endif #ifndef ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_INTERFACE #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_INTERFACE /** * @page page_iface_zwlr_foreign_toplevel_handle_v1 zwlr_foreign_toplevel_handle_v1 * @section page_iface_zwlr_foreign_toplevel_handle_v1_desc Description * * A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel * window. Each app may have multiple opened toplevels. * * Each toplevel has a list of outputs it is visible on, conveyed to the * client with the output_enter and output_leave events. * @section page_iface_zwlr_foreign_toplevel_handle_v1_api API * See @ref iface_zwlr_foreign_toplevel_handle_v1. */ /** * @defgroup iface_zwlr_foreign_toplevel_handle_v1 The zwlr_foreign_toplevel_handle_v1 interface * * A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel * window. Each app may have multiple opened toplevels. * * Each toplevel has a list of outputs it is visible on, conveyed to the * client with the output_enter and output_leave events. */ extern const struct wl_interface zwlr_foreign_toplevel_handle_v1_interface; #endif /** * @ingroup iface_zwlr_foreign_toplevel_manager_v1 * @struct zwlr_foreign_toplevel_manager_v1_listener */ struct zwlr_foreign_toplevel_manager_v1_listener { /** * a toplevel has been created * * This event is emitted whenever a new toplevel window is * created. It is emitted for all toplevels, regardless of the app * that has created them. * * All initial details of the toplevel(title, app_id, states, etc.) * will be sent immediately after this event via the corresponding * events in zwlr_foreign_toplevel_handle_v1. */ void (*toplevel)(void *data, struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1, struct zwlr_foreign_toplevel_handle_v1 *toplevel); /** * the compositor has finished with the toplevel manager * * This event indicates that the compositor is done sending * events to the zwlr_foreign_toplevel_manager_v1. The server will * destroy the object immediately after sending this request, so it * will become invalid and the client should free any resources * associated with it. */ void (*finished)(void *data, struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1); }; /** * @ingroup iface_zwlr_foreign_toplevel_manager_v1 */ static inline int zwlr_foreign_toplevel_manager_v1_add_listener(struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1, const struct zwlr_foreign_toplevel_manager_v1_listener *listener, void *data) { return wl_proxy_add_listener((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1, (void (**)(void)) listener, data); } #define ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_STOP 0 /** * @ingroup iface_zwlr_foreign_toplevel_manager_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_TOPLEVEL_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_manager_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_FINISHED_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_manager_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_STOP_SINCE_VERSION 1 /** @ingroup iface_zwlr_foreign_toplevel_manager_v1 */ static inline void zwlr_foreign_toplevel_manager_v1_set_user_data(struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1, user_data); } /** @ingroup iface_zwlr_foreign_toplevel_manager_v1 */ static inline void * zwlr_foreign_toplevel_manager_v1_get_user_data(struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1) { return wl_proxy_get_user_data((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1); } static inline uint32_t zwlr_foreign_toplevel_manager_v1_get_version(struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1) { return wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1); } /** @ingroup iface_zwlr_foreign_toplevel_manager_v1 */ static inline void zwlr_foreign_toplevel_manager_v1_destroy(struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1) { wl_proxy_destroy((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1); } /** * @ingroup iface_zwlr_foreign_toplevel_manager_v1 * * Indicates the client no longer wishes to receive events for new toplevels. * However the compositor may emit further toplevel_created events, until * the finished event is emitted. * * The client must not send any more requests after this one. */ static inline void zwlr_foreign_toplevel_manager_v1_stop(struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1) { wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1, ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_STOP, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1), 0); } #ifndef ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ENUM #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ENUM /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 * types of states on the toplevel * * The different states that a toplevel can have. These have the same meaning * as the states with the same names defined in xdg-toplevel */ enum zwlr_foreign_toplevel_handle_v1_state { /** * the toplevel is maximized */ ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED = 0, /** * the toplevel is minimized */ ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED = 1, /** * the toplevel is active */ ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED = 2, /** * the toplevel is fullscreen * @since 2 */ ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN = 3, }; /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN_SINCE_VERSION 2 #endif /* ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ENUM */ #ifndef ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ERROR_ENUM #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ERROR_ENUM enum zwlr_foreign_toplevel_handle_v1_error { /** * the provided rectangle is invalid */ ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ERROR_INVALID_RECTANGLE = 0, }; #endif /* ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ERROR_ENUM */ /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 * @struct zwlr_foreign_toplevel_handle_v1_listener */ struct zwlr_foreign_toplevel_handle_v1_listener { /** * title change * * This event is emitted whenever the title of the toplevel * changes. */ void (*title)(void *data, struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, const char *title); /** * app-id change * * This event is emitted whenever the app-id of the toplevel * changes. */ void (*app_id)(void *data, struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, const char *app_id); /** * toplevel entered an output * * This event is emitted whenever the toplevel becomes visible on * the given output. A toplevel may be visible on multiple outputs. */ void (*output_enter)(void *data, struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, struct wl_output *output); /** * toplevel left an output * * This event is emitted whenever the toplevel stops being * visible on the given output. It is guaranteed that an * entered-output event with the same output has been emitted * before this event. */ void (*output_leave)(void *data, struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, struct wl_output *output); /** * the toplevel state changed * * This event is emitted immediately after the * zlw_foreign_toplevel_handle_v1 is created and each time the * toplevel state changes, either because of a compositor action or * because of a request in this protocol. */ void (*state)(void *data, struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, struct wl_array *state); /** * all information about the toplevel has been sent * * This event is sent after all changes in the toplevel state * have been sent. * * This allows changes to the zwlr_foreign_toplevel_handle_v1 * properties to be seen as atomic, even if they happen via * multiple events. */ void (*done)(void *data, struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1); /** * this toplevel has been destroyed * * This event means the toplevel has been destroyed. It is * guaranteed there won't be any more events for this * zwlr_foreign_toplevel_handle_v1. The toplevel itself becomes * inert so any requests will be ignored except the destroy * request. */ void (*closed)(void *data, struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1); /** * parent change * * This event is emitted whenever the parent of the toplevel * changes. * * No event is emitted when the parent handle is destroyed by the * client. * @since 3 */ void (*parent)(void *data, struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, struct zwlr_foreign_toplevel_handle_v1 *parent); }; /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ static inline int zwlr_foreign_toplevel_handle_v1_add_listener(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, const struct zwlr_foreign_toplevel_handle_v1_listener *listener, void *data) { return wl_proxy_add_listener((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, (void (**)(void)) listener, data); } #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_MAXIMIZED 0 #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_MAXIMIZED 1 #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_MINIMIZED 2 #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_MINIMIZED 3 #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ACTIVATE 4 #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_CLOSE 5 #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_RECTANGLE 6 #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_DESTROY 7 #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_FULLSCREEN 8 #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_FULLSCREEN 9 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_TITLE_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_APP_ID_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_OUTPUT_ENTER_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_OUTPUT_LEAVE_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_DONE_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_CLOSED_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_PARENT_SINCE_VERSION 3 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_MAXIMIZED_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_MAXIMIZED_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_MINIMIZED_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_MINIMIZED_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ACTIVATE_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_CLOSE_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_RECTANGLE_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_DESTROY_SINCE_VERSION 1 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_FULLSCREEN_SINCE_VERSION 2 /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ #define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_FULLSCREEN_SINCE_VERSION 2 /** @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ static inline void zwlr_foreign_toplevel_handle_v1_set_user_data(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, user_data); } /** @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ static inline void * zwlr_foreign_toplevel_handle_v1_get_user_data(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) { return wl_proxy_get_user_data((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1); } static inline uint32_t zwlr_foreign_toplevel_handle_v1_get_version(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) { return wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1); } /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 * * Requests that the toplevel be maximized. If the maximized state actually * changes, this will be indicated by the state event. */ static inline void zwlr_foreign_toplevel_handle_v1_set_maximized(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) { wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0); } /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 * * Requests that the toplevel be unmaximized. If the maximized state actually * changes, this will be indicated by the state event. */ static inline void zwlr_foreign_toplevel_handle_v1_unset_maximized(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) { wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0); } /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 * * Requests that the toplevel be minimized. If the minimized state actually * changes, this will be indicated by the state event. */ static inline void zwlr_foreign_toplevel_handle_v1_set_minimized(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) { wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_MINIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0); } /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 * * Requests that the toplevel be unminimized. If the minimized state actually * changes, this will be indicated by the state event. */ static inline void zwlr_foreign_toplevel_handle_v1_unset_minimized(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) { wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_MINIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0); } /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 * * Request that this toplevel be activated on the given seat. * There is no guarantee the toplevel will be actually activated. */ static inline void zwlr_foreign_toplevel_handle_v1_activate(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, struct wl_seat *seat) { wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ACTIVATE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0, seat); } /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 * * Send a request to the toplevel to close itself. The compositor would * typically use a shell-specific method to carry out this request, for * example by sending the xdg_toplevel.close event. However, this gives * no guarantees the toplevel will actually be destroyed. If and when * this happens, the zwlr_foreign_toplevel_handle_v1.closed event will * be emitted. */ static inline void zwlr_foreign_toplevel_handle_v1_close(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) { wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_CLOSE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0); } /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 * * The rectangle of the surface specified in this request corresponds to * the place where the app using this protocol represents the given toplevel. * It can be used by the compositor as a hint for some operations, e.g * minimizing. The client is however not required to set this, in which * case the compositor is free to decide some default value. * * If the client specifies more than one rectangle, only the last one is * considered. * * The dimensions are given in surface-local coordinates. * Setting width=height=0 removes the already-set rectangle. */ static inline void zwlr_foreign_toplevel_handle_v1_set_rectangle(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, struct wl_surface *surface, int32_t x, int32_t y, int32_t width, int32_t height) { wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_RECTANGLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0, surface, x, y, width, height); } /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 * * Destroys the zwlr_foreign_toplevel_handle_v1 object. * * This request should be called either when the client does not want to * use the toplevel anymore or after the closed event to finalize the * destruction of the object. */ static inline void zwlr_foreign_toplevel_handle_v1_destroy(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) { wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), WL_MARSHAL_FLAG_DESTROY); } /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 * * Requests that the toplevel be fullscreened on the given output. If the * fullscreen state and/or the outputs the toplevel is visible on actually * change, this will be indicated by the state and output_enter/leave * events. * * The output parameter is only a hint to the compositor. Also, if output * is NULL, the compositor should decide which output the toplevel will be * fullscreened on, if at all. */ static inline void zwlr_foreign_toplevel_handle_v1_set_fullscreen(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, struct wl_output *output) { wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0, output); } /** * @ingroup iface_zwlr_foreign_toplevel_handle_v1 * * Requests that the toplevel be unfullscreened. If the fullscreen state * actually changes, this will be indicated by the state event. */ static inline void zwlr_foreign_toplevel_handle_v1_unset_fullscreen(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) { wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0); } #ifdef __cplusplus } #endif #endif dangvd-crystal-dock-7a34e96/src/display/wlr_window_manager.cc000066400000000000000000000251441512233520000243450ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "wlr_window_manager.h" #include #include namespace crystaldock { zwlr_foreign_toplevel_manager_v1* WlrWindowManager::window_manager_; std::unordered_map> WlrWindowManager::windows_; struct zwlr_foreign_toplevel_handle_v1* WlrWindowManager::activeWindow_; struct zwlr_foreign_toplevel_handle_v1* WlrWindowManager::activeWindowBeforeShowDesktop_; bool WlrWindowManager::showingDesktop_; /* static */ WlrWindowManager* WlrWindowManager::self() { static WlrWindowManager self; return &self; } /* static */ void WlrWindowManager::init( struct zwlr_foreign_toplevel_manager_v1* window_manager) { window_manager_ = window_manager; zwlr_foreign_toplevel_manager_v1_add_listener( window_manager_, &window_manager_listener_, NULL); connect(WlrWindowManager::self(), &WlrWindowManager::activeWindowChanged, WindowSystem::self(), &WindowSystem::activeWindowChanged); connect(WlrWindowManager::self(), &WlrWindowManager::windowAdded, WindowSystem::self(), &WindowSystem::windowAdded); connect(WlrWindowManager::self(), &WlrWindowManager::windowGeometryChanged, WindowSystem::self(), &WindowSystem::windowGeometryChanged); connect(WlrWindowManager::self(), &WlrWindowManager::windowLeftCurrentDesktop, WindowSystem::self(), &WindowSystem::windowLeftCurrentDesktop); connect(WlrWindowManager::self(), &WlrWindowManager::windowRemoved, WindowSystem::self(), &WindowSystem::windowRemoved); connect(WlrWindowManager::self(), &WlrWindowManager::windowStateChanged, WindowSystem::self(), &WindowSystem::windowStateChanged); connect(WlrWindowManager::self(), &WlrWindowManager::windowTitleChanged, WindowSystem::self(), &WindowSystem::windowTitleChanged); connect(WlrWindowManager::self(), &WlrWindowManager::windowEnteredOutput, WindowSystem::self(), &WindowSystem::windowEnteredOutput); connect(WlrWindowManager::self(), &WlrWindowManager::windowLeftOutput, WindowSystem::self(), &WindowSystem::windowLeftOutput); } /* static */ void WlrWindowManager::bindWindowManagerFunctions( WindowManager* windowManager) { windowManager->activateOrMinimizeWindow = WlrWindowManager::activateOrMinimizeWindow; windowManager->activateWindow = WlrWindowManager::activateWindow; windowManager->minimizeWindow = WlrWindowManager::minimizeWindow; windowManager->activeWindow = WlrWindowManager::activeWindow; windowManager->closeWindow = WlrWindowManager::closeWindow; windowManager->resetActiveWindow = WlrWindowManager::resetActiveWindow; windowManager->windows = WlrWindowManager::windows; windowManager->setShowingDesktop = WlrWindowManager::setShowingDesktop; windowManager->showingDesktop = WlrWindowManager::showingDesktop; } /* static */ std::vector WlrWindowManager::windows() { std::vector windows; for (const auto& element : windows_) { if (element.second) { windows.push_back(element.second.get()); } } std::sort(windows.begin(), windows.end(), [](const WindowInfo* w1, const WindowInfo* w2) { return w1->mapping_order < w2->mapping_order; }); return windows; } /* static */ void* WlrWindowManager::activeWindow() { return activeWindow_; } /* static */ void WlrWindowManager::resetActiveWindow() { activeWindow_ = nullptr; emit WlrWindowManager::self()->activeWindowChanged(nullptr); } /* static */ void WlrWindowManager::activateWindow(void* window_handle) { auto* window = static_cast(window_handle); if (!window) { return; } auto app = dynamic_cast(QGuiApplication::instance()); auto waylandApp = app->nativeInterface(); if (!waylandApp) { return; } auto seat = waylandApp->seat(); if (!seat) { return; } zwlr_foreign_toplevel_handle_v1_activate(window, seat); } /* static */ void WlrWindowManager::activateOrMinimizeWindow(void* window_handle) { auto* window = static_cast(window_handle); if (window) { if (windows_.count(window) == 0) { return; } if (windows_[window]->minimized || window != activeWindow_) { activateWindow(window); } else { zwlr_foreign_toplevel_handle_v1_set_minimized(window); } } } /* static */ void WlrWindowManager::minimizeWindow(void* window_handle) { auto* window = static_cast(window_handle); if (window) { if (windows_.count(window) == 0) { return; } zwlr_foreign_toplevel_handle_v1_set_minimized(window); } } /*static*/ void WlrWindowManager::closeWindow(void* window_handle) { auto* window = static_cast(window_handle); if (window) { zwlr_foreign_toplevel_handle_v1_close(window); } } /* static */ bool WlrWindowManager::showingDesktop() { return showingDesktop_; } /* static */ void WlrWindowManager::setShowingDesktop(bool show) { for (const auto& [window, windowInfo] : windows_) { if (show) { windowInfo->restoreAfterShowDesktop = !windowInfo->minimized; if (!windowInfo->minimized) { zwlr_foreign_toplevel_handle_v1_set_minimized(window); } if (activeWindow_) { activeWindowBeforeShowDesktop_ = activeWindow_; } showingDesktop_ = true; } else { if (windowInfo->restoreAfterShowDesktop) { activateWindow(window); } if (activeWindowBeforeShowDesktop_) { activateWindow(activeWindowBeforeShowDesktop_); } showingDesktop_ = false; } } } // zwlr_foreign_toplevel_manager_v1 interface. /* static */ void WlrWindowManager::toplevel( void *data, struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1, struct zwlr_foreign_toplevel_handle_v1 *window) { windows_[window] = std::make_unique(); windows_[window]->window = window; static uint32_t mapping_order = 0; windows_[window]->mapping_order = mapping_order++; zwlr_foreign_toplevel_handle_v1_add_listener(window, &window_listener_, NULL); } /* static */ void WlrWindowManager::finished( void *data, struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1) {} // zwlr_foreign_toplevel_handle_v1 interface. /* static */ void WlrWindowManager::title( void *data, struct zwlr_foreign_toplevel_handle_v1 *window, const char *title) { if (windows_.count(window) == 0) { return; } windows_[window]->title = title; if (windows_[window]->initialized) { emit self()->windowTitleChanged(windows_[window].get()); } } /* static */ void WlrWindowManager::app_id( void *data, struct zwlr_foreign_toplevel_handle_v1 *window, const char *app_id) { if (windows_.count(window) == 0) { return; } windows_[window]->appId = app_id; } /* static */ void WlrWindowManager::output_enter( void *data, struct zwlr_foreign_toplevel_handle_v1 *window, struct wl_output *output) { if (windows_.count(window) == 0) { return; } windows_[window]->outputs.insert(output); emit self()->windowEnteredOutput(windows_[window].get(), output); } /* static */ void WlrWindowManager::output_leave( void *data, struct zwlr_foreign_toplevel_handle_v1 *window, struct wl_output *output) { if (windows_.count(window) == 0) { return; } windows_[window]->outputs.erase(output); emit self()->windowLeftOutput(windows_[window].get(), output); } /* static */ void WlrWindowManager::state( void *data, struct zwlr_foreign_toplevel_handle_v1 *window, struct wl_array *state) { if (windows_.count(window) == 0) { return; } windows_[window]->minimized = false; windows_[window]->maximized = false; windows_[window]->fullscreen = false; for (uint32_t* entry = static_cast(state->data); state->size != 0 && reinterpret_cast(entry) < static_cast(state->data) + state->size; ++entry) { if (*entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED) { if (!windows_[window]->minimized) { windows_[window]->maximized = true; } } if (*entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED) { windows_[window]->minimized = true; windows_[window]->maximized = false; windows_[window]->fullscreen = false; if (activeWindow_ == window) { activeWindow_ = nullptr; if (windows_[window]->initialized) { emit self()->activeWindowChanged(activeWindow_); } } // This is to fix the case where a window could be Minimized // and Activated at the same time on Wayfire. break; } if (*entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED) { if (activeWindow_ != window) { activeWindow_ = window; if (windows_[window]->initialized) { emit self()->activeWindowChanged(activeWindow_); } } } if (*entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN) { if (!windows_[window]->minimized) { windows_[window]->fullscreen = true; } } } if (windows_[window]->initialized) { emit self()->windowStateChanged(windows_[window].get()); } } /* static */ void WlrWindowManager::done( void *data, struct zwlr_foreign_toplevel_handle_v1 *window) { if (windows_.count(window) == 0) { return; } windows_[window]->initialized = true; emit self()->windowAdded(windows_[window].get()); } /* static */ void WlrWindowManager::closed( void *data, struct zwlr_foreign_toplevel_handle_v1 *window) { if (windows_.count(window) == 0) { return; } emit self()->windowRemoved(windows_[window]->window); windows_.erase(window); } /* static */ void WlrWindowManager::parent( void *data, struct zwlr_foreign_toplevel_handle_v1 *window, struct zwlr_foreign_toplevel_handle_v1 *parent) {} } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/display/wlr_window_manager.h000066400000000000000000000110571512233520000242050ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef WLR_WINDOW_MANAGER_H_ #define WLR_WINDOW_MANAGER_H_ #include #include #include "window_system.h" #include "wlr_foreign_toplevel_management.h" namespace crystaldock { class WlrWindowManager : public QObject { Q_OBJECT private: WlrWindowManager() = default; signals: void windowAdded(const WindowInfo*); void windowRemoved(void*); void windowLeftCurrentDesktop(void*); void windowGeometryChanged(const WindowInfo*); void windowStateChanged(const WindowInfo*); void windowTitleChanged(const WindowInfo*); void activeWindowChanged(void*); // Signals emitted when a window entered or left an output (screen). void windowEnteredOutput(const WindowInfo*, const wl_output*); void windowLeftOutput(const WindowInfo*, const wl_output*); public: static WlrWindowManager* self(); static void init(struct zwlr_foreign_toplevel_manager_v1* window_manager); static void bindWindowManagerFunctions(WindowManager* windowManager); static std::vector windows(); static void* activeWindow(); // We manually reset active window, usually when the new active window is the dock itself. // We don't want to always do this (e.g. handle this in state_change() handler) because // otherwise we wouldn't be able to click on an active window's icon to minimize it // (the click action would change the active window to be the dock). static void resetActiveWindow(); static void activateWindow(void* window); static void activateOrMinimizeWindow(void* window); static void minimizeWindow(void* window); static void closeWindow(void* window); static bool showingDesktop(); static void setShowingDesktop(bool show); private: // zwlr_foreign_toplevel_manager_v1 interface. static void toplevel( void *data, struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1, struct zwlr_foreign_toplevel_handle_v1 *window); static void finished( void *data, struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1); // zwlr_foreign_toplevel_handle_v1 interface. static void title( void *data, struct zwlr_foreign_toplevel_handle_v1 *window, const char *title); static void app_id( void *data, struct zwlr_foreign_toplevel_handle_v1 *window, const char *app_id); static void output_enter( void *data, struct zwlr_foreign_toplevel_handle_v1 *window, struct wl_output *output); static void output_leave( void *data, struct zwlr_foreign_toplevel_handle_v1 *window, struct wl_output *output); static void state( void *data, struct zwlr_foreign_toplevel_handle_v1 *window, struct wl_array *state); static void done( void *data, struct zwlr_foreign_toplevel_handle_v1 *window); static void closed( void *data, struct zwlr_foreign_toplevel_handle_v1 *window); static void parent( void *data, struct zwlr_foreign_toplevel_handle_v1 *window, struct zwlr_foreign_toplevel_handle_v1 *parent); static constexpr struct zwlr_foreign_toplevel_manager_v1_listener window_manager_listener_ = { toplevel, finished }; static constexpr struct zwlr_foreign_toplevel_handle_v1_listener window_listener_ = { title, app_id, output_enter, output_leave, state, done, closed, parent, }; static zwlr_foreign_toplevel_manager_v1* window_manager_; static std::unordered_map> windows_; static struct zwlr_foreign_toplevel_handle_v1* activeWindow_; // So when we show desktop on/off we can restore the active window. static struct zwlr_foreign_toplevel_handle_v1* activeWindowBeforeShowDesktop_; static bool showingDesktop_; }; } #endif // WLR_WINDOW_MANAGER_H_ dangvd-crystal-dock-7a34e96/src/main.cc000066400000000000000000000066631512233520000177440ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include #include #include #include #include #include #include #include #include namespace { void maybeCopyOldConfigOnFirstRun(const QString& configDir) { // On the first run, copies the dock config from the old config location if it exists. // This is for backward compatibility. if (!QDir::home().exists(configDir)) { const auto oldConfigDir = QDir::homePath() + "/.crystal-dock-2"; if (QDir::home().exists(oldConfigDir)) { std::filesystem::copy(oldConfigDir.toStdString(), configDir.toStdString(), std::filesystem::copy_options::recursive); std::cout << "Copied config from " << oldConfigDir.toStdString() << " to " << configDir.toStdString() << std::endl; } } } void maybeCopyPresetConfigOnFirstRun(const QString& configDir) { // On the first run, copies the dock config from XDG_CONFIG_DIRS if it exists. // This is mainly for package managers to pre-configure the dock. if (!QDir::home().exists(configDir)) { QStringList xdgConfigDirs = qEnvironmentVariable("XDG_CONFIG_DIRS").split(":", Qt::SkipEmptyParts); for (const auto& xdgConfigDir : xdgConfigDirs) { const auto srcDir = xdgConfigDir + "/crystal-dock"; if (QDir::home().exists(srcDir)) { std::filesystem::copy(srcDir.toStdString(), configDir.toStdString(), std::filesystem::copy_options::recursive); std::cout << "Copied config from " << srcDir.toStdString() << " to " << configDir.toStdString() << std::endl; return; } } } } } int main(int argc, char** argv) { QApplication app(argc, argv); // Enforces single instance. QSharedMemory sharedMemory; sharedMemory.setKey("crystal-dock-key"); if (!sharedMemory.create(1 /*byte*/)) { // The failure might have been caused by a previous crash. sharedMemory.attach(); sharedMemory.detach(); // Now try again. if (!sharedMemory.create(1 /*byte*/)) { std::cerr << "Another instance is already running." << std::endl; return -1; } } if (!crystaldock::MultiDockView::checkPlatformSupported(app)) { return -1; } QApplication::setWindowIcon(QIcon::fromTheme("user-desktop")); auto configHome = qEnvironmentVariable("XDG_CONFIG_HOME").trimmed(); if (configHome.isEmpty()) { configHome = QDir::homePath() + "/.config"; } const auto configDir = configHome + "/crystal-dock"; maybeCopyOldConfigOnFirstRun(configDir); maybeCopyPresetConfigOnFirstRun(configDir); crystaldock::MultiDockModel model(configDir); crystaldock::MultiDockView view(&model); view.show(); return app.exec(); } dangvd-crystal-dock-7a34e96/src/model/000077500000000000000000000000001512233520000175765ustar00rootroot00000000000000dangvd-crystal-dock-7a34e96/src/model/application_menu_config.cc000066400000000000000000000222411512233520000247620ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "application_menu_config.h" #include #include #include #include #include #include #include #include #include namespace crystaldock { namespace { bool isHidden(const DesktopFile& desktopFile) { if (desktopFile.noDisplay() || desktopFile.hidden()) { return true; } QString desktopEnvName = DesktopEnv::getDesktopEnvName(); // Some desktop files still use the legacy "X-Desktop" name. if (!desktopFile.showOnDesktop(desktopEnvName) && !desktopFile.showOnDesktop("X-" + desktopEnvName)) { return true; } // Do not show LXQt special entries (Log Out / Reboot etc.) in the standard categories, // as they are already available in special sections (Session / Power). if (desktopFile.exec().startsWith("lxqt-leave")) { return true; } if (desktopFile.categories().isEmpty()) { return true; } return false; } } // namespace ApplicationMenuConfig::ApplicationMenuConfig(const QStringList& entryDirs) : entryDirs_(entryDirs), fileWatcher_(entryDirs), desktopEnv_(DesktopEnv::getDesktopEnv()) { initCategories(); initSystemCategories(); loadEntries(); connect(&fileWatcher_, SIGNAL(directoryChanged(const QString&)), this, SLOT(reload())); connect(&fileWatcher_, SIGNAL(fileChanged(const QString&)), this, SLOT(reload())); } QStringList ApplicationMenuConfig::getEntryDirs() { QStringList entryDirs{QDir::homePath() + "/.local/share/applications"}; QStringList dataDirs = qEnvironmentVariable("XDG_DATA_DIRS").split(":", Qt::SkipEmptyParts); if (dataDirs.empty()) { dataDirs.append("/usr/share/"); dataDirs.append("/usr/local/share/"); } for (const auto& dir : dataDirs) { entryDirs.append(dir + "/applications"); } return entryDirs; } void ApplicationMenuConfig::initCategories() { // We use the main categories as defined in: // https://specifications.freedesktop.org/menu-spec/latest/apa.html // plus a special Uncategorized category. static constexpr int kNumCategories = 12; static const char* const kCategories[kNumCategories][3] = { // Name, display name, icon. // Sorted by display name. {"Development", "Development", "applications-development"}, {"Education", "Education", "applications-science"}, {"Game", "Games", "applications-games"}, {"Graphics", "Graphics", "applications-graphics"}, {"Network", "Internet", "applications-internet"}, {"AudioVideo", "Multimedia", "applications-multimedia"}, {"Office", "Office", "applications-office"}, {"Science", "Science", "applications-science"}, {"Settings", "Settings", "preferences-system"}, {"System", "System", "applications-system"}, {"Utility", "Utilities", "applications-utilities"}, // Uncategorized is not visible anyway. {kUncategorized, kUncategorized, "applications-other"}, }; categories_.reserve(kNumCategories); for (int i = 0; i < kNumCategories; ++i) { categories_.push_back(Category( kCategories[i][0], kCategories[i][1], kCategories[i][2])); categoryMap_[kCategories[i][0]] = i; } } void ApplicationMenuConfig::initSystemCategories() { systemCategories_ = desktopEnv_->getApplicationMenuSystemCategories(); } void ApplicationMenuConfig::clearEntries() { for (auto& category : categories_) { category.entries.clear(); } entries_.clear(); shortAppIds_.clear(); wmClasses_.clear(); names_.clear(); } bool ApplicationMenuConfig::loadEntries() { for (const QString& entryDir : entryDirs_) { if (!QDir::root().exists(entryDir)) { continue; } QDir dir(entryDir); QStringList files = dir.entryList({"*.desktop"}, QDir::Files, QDir::Name); if (files.isEmpty()) { continue; } for (int i = 0; i < files.size(); ++i) { const QString& file = entryDir + "/" + files.at(i); loadEntry(file); } } return true; } bool ApplicationMenuConfig::loadEntry(const QString &file) { DesktopFile desktopFile(file); if (desktopFile.type() != "Application") { return false; } auto categories = desktopFile.categories(); if (categories.isEmpty()) { categories = {kUncategorized}; } const QString appId = desktopFile.appId(); for (int i = 0; i < categories.size(); ++i) { const std::string category = categories[i].toStdString(); if (categoryMap_.count(category) > 0 && entries_.count(appId.toStdString()) == 0) { const QString command = filterFieldCodes(desktopFile.exec().simplified()); ApplicationEntry newEntry(appId, desktopFile.name(), desktopFile.genericName(), desktopFile.icon(), command, file, isHidden(desktopFile)); auto& entries = categories_[categoryMap_[category]].entries; auto next = std::lower_bound(entries.begin(), entries.end(), newEntry); entries.insert(next, newEntry); auto* entry = &(*--next); const auto appId = newEntry.appId.toLower(); entries_[appId.toStdString()] = entry; auto shortAppId = appId.simplified().replace(" ", ""); shortAppId = shortAppId.mid(shortAppId.lastIndexOf('.') + 1); shortAppIds_[shortAppId.toStdString()] = entry; const auto shortCommand = getShortCommand(command).toLower().toStdString(); if (!shortCommand.empty()) { commands_[shortCommand] = entry; } const auto wmClass = desktopFile.wmClass().toLower().simplified().replace(" ", "").toStdString(); if (!wmClass.empty()) { wmClasses_[wmClass] = entry; } const auto name = desktopFile.name().toLower().simplified().replace(" ", "").toStdString(); if (!name.empty()) { names_[name] = entry; } } } return true; } void ApplicationMenuConfig::reload() { clearEntries(); loadEntries(); emit configChanged(); } const ApplicationEntry* ApplicationMenuConfig::findApplication(const std::string& appId) const { for (const auto& category : systemCategories_) { for (const auto& entry : category.entries) { if (entry.appId.toStdString() == appId) { return &entry; } } } return entries_.count(appId) > 0 ? entries_.at(appId) : shortAppIds_.count(appId) > 0 ? shortAppIds_.at(appId) : commands_.count(appId) > 0 ? commands_.at(appId) : wmClasses_.count(appId) > 0 ? wmClasses_.at(appId) : names_.count(appId) > 0 ? names_.at(appId) : nullptr; } const ApplicationEntry* ApplicationMenuConfig::tryMatchingApplicationId( const std::string& appId) const { QString id = QString::fromStdString(appId).toLower(); if (auto* app = findApplication(id.toStdString())) { return app; } id = id.simplified().replace(" ", ""); if (auto* app = findApplication(id.toStdString())) { return app; } id = id.mid(id.lastIndexOf('.') + 1); if (auto* app = findApplication(id.toStdString())) { return app; } // Special fix for Qt6 D-Bus Viewer. if (id == "qdbusviewer") { if (auto* app = findApplication("org.qt.qdbusviewer6")) { return app; } } // Special fix for VirtualBox. if (id == "virtualboxvm" || id == "virtualboxmachine" || id == "virtualboxmanager") { if (auto* app = findApplication("virtualbox")) { return app; } } // Special fix for Google Chrome Flatpak. if (id == "google-chrome") { if (auto* app = findApplication("com.google.chrome")) { return app; } } return nullptr; } const std::vector ApplicationMenuConfig::searchApplications( const QString& text, unsigned int maxNumResults) const { std::vector entries; for (const auto& category : categories_) { for (const auto& entry : category.entries) { if (text.length() == 1) { if (entry.name.startsWith(text, Qt::CaseInsensitive)) { entries.push_back(entry); } } else { if (entry.name.contains(text, Qt::CaseInsensitive) || entry.genericName.contains(text, Qt::CaseInsensitive)) { entries.push_back(entry); } } if (entries.size() == maxNumResults) { goto end_loop; } } } end_loop: std::sort(entries.begin(), entries.end()); return entries; } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/model/application_menu_config.h000066400000000000000000000101121512233520000246160ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_APPLICATION_MENU_CONFIG_H_ #define CRYSTALDOCK_APPLICATION_MENU_CONFIG_H_ #include #include #include #include #include #include #include #include #include #include #include "application_menu_entry.h" #include namespace crystaldock { class ApplicationMenuConfig : public QObject { Q_OBJECT public: static constexpr char kUncategorized[] = "Uncategorized"; ApplicationMenuConfig(const QStringList& entryDirs = getEntryDirs()); ~ApplicationMenuConfig() = default; static QStringList getEntryDirs(); const std::vector& categories() const { return categories_; } const std::vector& systemCategories() const { return systemCategories_; } // Finds the application entry given the application ID. // Will match with each of App ID, short command, WM Class and Name in the // entry list in that order. const ApplicationEntry* findApplication(const std::string& appId) const; bool isAppMenuEntry(const std::string& appId) const { return entries_.count(appId) > 0; } // Tries to find a matching application ID using different heuristics. const ApplicationEntry* tryMatchingApplicationId(const std::string& appId) const; // Searches for applications with the name containing the given text. const std::vector searchApplications( const QString& text, unsigned int maxNumResults) const; signals: void configChanged(); public slots: void reload(); private: // Initializes application categories. void initCategories(); // Initializes system categories. void initSystemCategories(); // Clears all application entries. void clearEntries(); // Loads application entries from entryDir. bool loadEntries(); // Loads an application entry from the .desktop file. bool loadEntry(const QString& file); // The directories that contains the list of all application entries as // desktop files, e.g. /usr/share/applications const QStringList entryDirs_; // Application entries, organized by categories. std::vector categories_; // System entries (e.g. Lock Screen / Shut Down), organized by categories. std::vector systemCategories_; // Map from category names to category indices in the above vector, // to make loading entries faster. std::unordered_map categoryMap_; // Map from app ids to application entries for fast look-up. std::unordered_map entries_; // Map from a short-form app ids to application entries for fast look-up. std::unordered_map shortAppIds_; // Map from short commands to application entries for fast look-up. // Short command means for example, "command" instead of "/usr/bin/command -a -b" std::unordered_map commands_; // Map from WM classes to application entries for fast look-up. std::unordered_map wmClasses_; // Map from names to application entries for fast look-up. std::unordered_map names_; QFileSystemWatcher fileWatcher_; DesktopEnv* desktopEnv_; friend class ApplicationMenuConfigTest; }; } #endif // CRYSTALDOCK_APPLICATION_MENU_CONFIG_H_ dangvd-crystal-dock-7a34e96/src/model/application_menu_config_test.cc000066400000000000000000000227111512233520000260230ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "application_menu_config.h" #include #include #include #include #include #include namespace crystaldock { class ApplicationMenuConfigTest: public QObject { Q_OBJECT private slots: void init() { QTemporaryDir configDir; } void loadEntries_singleDir(); void loadEntries_multipleDirs(); void tryMatchingApplicationId(); private: void writeEntry(const QString& filename, const ApplicationEntry& entry, const QString& categories, const std::unordered_map& extraKVs = {}) { DesktopFile desktopFile; desktopFile.setName(entry.name); desktopFile.setGenericName(entry.genericName); desktopFile.setIcon(entry.icon); desktopFile.setExec(entry.command); desktopFile.setType("Application"); desktopFile.setCategories(categories); for (const auto& entry : extraKVs) { if (entry.first == "Hidden") { desktopFile.setHidden(entry.second == "true"); } else if (entry.first == "NoDisplay") { desktopFile.setNoDisplay(entry.second == "true"); } else if (entry.first == "OnlyShowIn") { desktopFile.setOnlyShowIn(QString::fromStdString(entry.second)); } else if (entry.first == "NotShowIn") { desktopFile.setNotShowIn(QString::fromStdString(entry.second)); } else if (entry.first == "StartupWMClass") { desktopFile.setWMClass(QString::fromStdString(entry.second)); } } desktopFile.write(filename); } }; void ApplicationMenuConfigTest::loadEntries_singleDir() { QTemporaryDir entryDir; QVERIFY(entryDir.isValid()); writeEntry(entryDir.path() + "/chrome.desktop", {"chrome", "Chrome", "Web Browser", "chrome", "chrome", ""}, "Network"); ApplicationMenuConfig config({ entryDir.path() }); QCOMPARE(config.entries_.size(), 1); for (const auto& category : config.categories_) { if (category.name == "Network") { QCOMPARE(static_cast(category.entries.size()), 1); } else { QCOMPARE(static_cast(category.entries.size()), 0); } } } void ApplicationMenuConfigTest::loadEntries_multipleDirs() { QTemporaryDir entryDir1; QVERIFY(entryDir1.isValid()); writeEntry(entryDir1.path() + "/chrome.desktop", {"chrome", "Chrome", "Web Browser", "chrome", "chrome", ""}, "Network"); writeEntry(entryDir1.path() + "/mail.desktop", {"mail", "Mail", "Email Client", "mail", "mail", ""}, "Network;Office"); writeEntry(entryDir1.path() + "/adesktop-settings.desktop", {"adesktop-settings", "ADesktop Settings", "", "adesktop-settings", "adesktop-settings", ""}, "Settings", {{"OnlyShowIn", "ADesktop"}}); // Empty dir QTemporaryDir entryDir2; QVERIFY(entryDir2.isValid()); QTemporaryDir entryDir3; QVERIFY(entryDir3.isValid()); writeEntry(entryDir3.path() + "/systemsettings.desktop", {"systemsettings", "System Settings", "", "systemsettings", "systemsettings", ""}, "Settings", {{"OnlyShowIn", DesktopEnv::getDesktopEnvName().toStdString()}}); writeEntry(entryDir3.path() + "/bdesktop-settings.desktop", {"bdesktop-settings", "BDesktop Settings", "", "bdesktop-settings", "bdesktop-settings", ""}, "Settings", {{"NotShowIn", DesktopEnv::getDesktopEnvName().toStdString()}}); writeEntry(entryDir3.path() + "/chrome-html.desktop", {"chrome-html", "Chrome - HTML", "Web Browser", "chrome", "chrome", ""}, "Network", {{"NoDisplay", "true"}}); writeEntry(entryDir3.path() + "/chrome-old.desktop", {"chrome-old", "Chrome - Old", "Web Browser", "chrome", "chrome", ""}, "Network", {{"Hidden", "true"}}); writeEntry(entryDir3.path() + "/lxqt-shutdown.desktop", {"lxqt-shutdown", "Shutdown", "Shutdown", "system-shutdown", "lxqt-leave --shutdown", ""}, "System"); ApplicationMenuConfig config( { entryDir1.path(), entryDir1.path() + "/dir-not-exist", entryDir2.path(), entryDir3.path() }); QCOMPARE(config.entries_.size(), 8); int numHidden = 0; for (const auto& pair : config.entries_) { if (pair.second->hidden) { numHidden++; } } QCOMPARE(numHidden, 4); for (const auto& category : config.categories_) { if (category.name == "Network") { QCOMPARE(static_cast(category.entries.size()), 4); } else if (category.name == "Settings") { QCOMPARE(static_cast(category.entries.size()), 3); } else if (category.name == "System") { QCOMPARE(static_cast(category.entries.size()), 1); } else { QCOMPARE(static_cast(category.entries.size()), 0); } } } void ApplicationMenuConfigTest::tryMatchingApplicationId() { QTemporaryDir entryDir; QVERIFY(entryDir.isValid()); writeEntry(entryDir.path() + "/firefox.desktop", {"firefox", "Firefox", "Web Browser", "firefox", "firefox", ""}, "Network"); writeEntry(entryDir.path() + "/org.kde.konsole.desktop", {"org.kde.konsole", "Konsole", "Terminal", "konsole", "konsole", ""}, "System"); writeEntry(entryDir.path() + "/google-chrome.desktop", {"google-chrome", "Chrome", "Web Browser", "google-chrome", "google-chrome", ""}, "Network"); writeEntry(entryDir.path() + "/org.kde.krita.desktop", {"org.kde.krita", "Krita", "Digital Painting", "krita", "Krita", ""}, "Graphics"); writeEntry(entryDir.path() + "/gimp.desktop", {"gimp", "GIMP", "Image Editor", "gimp", "GIMP", ""}, "Graphics", {{"StartupWMClass", "gimp-2.10"}}); writeEntry(entryDir.path() + "/org.qt.qdbusviewer6.desktop", {"org.qt.qdbusviewer6", "Qt 6 D-Bus Viewer", "D-Bus Debugger", "qdbusviewer6", "Qt 6 D-Bus Viewer", ""}, "Development"); writeEntry(entryDir.path() + "/virtualbox.desktop", {"virtualbox", "Virtual Box", "Virtualization", "virtualbox", "Virtual Box", ""}, "Utility"); writeEntry(entryDir.path() + "/io.sourceforge.ChessX.desktop", {"io.sourceforge.ChessX", "ChessX", "Chess Game", "io.sourceforge.ChessX", "chessx", ""}, "Game"); writeEntry(entryDir.path() + "/Libre Menu Editor.desktop", {"Libre Menu Editor", "Libre Menu Editor", "Menu Editor", "Libre Menu Editor", "Libre Menu Editor", ""}, "Utility"); writeEntry(entryDir.path() + "/Sonic Unleashed Shortcut.desktop", {"Sonic Unleashed", "Sonic Unleashed", "Sonic Game", "Sonic Unleashed", "/applications/UnleashedRecomp/UnleashedRecomp-1.0.AppImage", ""}, "Game"); ApplicationMenuConfig config({ entryDir.path() }); QCOMPARE(config.entries_.size(), 10); QVERIFY(config.tryMatchingApplicationId("firefox")); QCOMPARE(config.tryMatchingApplicationId("firefox")->appId, "firefox"); QVERIFY(config.tryMatchingApplicationId("org.kde.konsole")); QCOMPARE(config.tryMatchingApplicationId("org.kde.konsole")->appId, "org.kde.konsole"); QVERIFY(config.tryMatchingApplicationId("Google-chrome")); QCOMPARE(config.tryMatchingApplicationId("Google-chrome")->appId, "google-chrome"); QVERIFY(config.tryMatchingApplicationId("krita")); QCOMPARE(config.tryMatchingApplicationId("krita")->appId, "org.kde.krita"); QVERIFY(config.tryMatchingApplicationId("Gimp-2.10")); QCOMPARE(config.tryMatchingApplicationId("Gimp-2.10")->appId, "gimp"); QVERIFY(config.tryMatchingApplicationId("qdbusviewer")); QCOMPARE(config.tryMatchingApplicationId("qdbusviewer")->appId, "org.qt.qdbusviewer6"); QVERIFY(config.tryMatchingApplicationId("virtualboxvm")); QCOMPARE(config.tryMatchingApplicationId("virtualboxvm")->appId, "virtualbox"); QVERIFY(config.tryMatchingApplicationId("net.sourceforge.chessx.chessx")); QCOMPARE(config.tryMatchingApplicationId("net.sourceforge.chessx.chessx")->appId, "io.sourceforge.chessx"); QVERIFY(config.tryMatchingApplicationId( "page.codeberg.libre_menu_editor.LibreMenuEditor")); QCOMPARE(config.tryMatchingApplicationId( "page.codeberg.libre_menu_editor.LibreMenuEditor")->appId, "libre menu editor"); QVERIFY(config.tryMatchingApplicationId("UnleashedRecomp")); QCOMPARE(config.tryMatchingApplicationId("UnleashedRecomp")->appId, "sonic unleashed shortcut"); } } // namespace crystaldock QTEST_GUILESS_MAIN(crystaldock::ApplicationMenuConfigTest) #include "application_menu_config_test.moc" dangvd-crystal-dock-7a34e96/src/model/application_menu_entry.h000066400000000000000000000054631512233520000245270ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_APPLICATION_MENU_ENTRY_H_ #define CRYSTALDOCK_APPLICATION_MENU_ENTRY_H_ #include #include namespace crystaldock { // An application entry in the application menu. struct ApplicationEntry { // App ID e.g. org.kde.dolphin QString appId; // Name e.g. 'Chrome'. QString name; // Generic name e.g. 'Web Brower'. QString genericName; // Icon name e.g. 'chrome'. QString icon; // Command to execute e.g. '/usr/bin/google-chrome-stable'. QString command; // The path to the desktop file e.g. '/usr/share/applications/google-chrome.desktop' QString desktopFile; // If it's hidden, it won't show on the Application Menu. bool hidden; ApplicationEntry(const QString& appId2, const QString& name2, const QString& genericName2, const QString& icon2, const QString& command2, const QString& desktopFile2, bool hidden2 = false) : appId(appId2), name(name2), genericName(genericName2), icon(icon2), command(command2), desktopFile(desktopFile2), hidden(hidden2) {} }; inline bool operator<(const ApplicationEntry &e1, const ApplicationEntry &e2) { return e1.name.toLower() < e2.name.toLower(); } // A category in the application menu. struct Category { // Name for the category e.g. 'Development' or 'Utility'. See: // https://specifications.freedesktop.org/menu-spec/latest/apa.html QString name; // Display name for the category e.g. 'Utilities'. QString displayName; // Icon name for the category e.g. 'applications-internet'. QString icon; // Application entries for this category. std::list entries; Category(const QString& name2, const QString& displayName2, const QString& icon2) : name(name2), displayName(displayName2), icon(icon2) {} Category(const QString& name2, const QString& displayName2, const QString& icon2, std::list entries2) : name(name2), displayName(displayName2), icon(icon2), entries(entries2) { } }; } // namespace crystaldock #endif // CRYSTALDOCK_APPLICATION_MENU_ENTRY_H_ dangvd-crystal-dock-7a34e96/src/model/config_helper.cc000066400000000000000000000035231512233520000227140ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "config_helper.h" #include #include #include "desktop/desktop_env.h" namespace crystaldock { constexpr char ConfigHelper::kConfigPattern[]; constexpr char ConfigHelper::kAppearanceConfig[]; // Creates a seperate config dir for each desktop environment. ConfigHelper::ConfigHelper(const QString& configDir) : configDir_{configDir + "/" + DesktopEnv::getDesktopEnvName()} { if (!configDir_.exists()) { QDir::root().mkpath(configDir_.path()); } } std::vector ConfigHelper::findAllDockConfigs() const { std::vector allConfigs; QStringList files = configDir_.entryList( {kConfigPattern}, QDir::Files, QDir::Name); if (files.isEmpty()) { return allConfigs; } for (int i = 0; i < files.size(); ++i) { const QString& configFile = files.at(i); allConfigs.push_back(dockConfigPath(configFile)); } return allConfigs; } QString ConfigHelper::findNextDockConfig() const { for (int fileId = 1; ; ++fileId) { if (!configDir_.exists(dockConfigFile(fileId))) { return dockConfigPath(fileId); } } } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/model/config_helper.h000066400000000000000000000045631512233520000225630ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_CONFIG_HELPER_H_ #define CRYSTALDOCK_CONFIG_HELPER_H_ #include #include namespace crystaldock { // Helper class for working with configurations. class ConfigHelper { public: // Individual dock configs. static constexpr char kConfigPattern[] = "panel_*.conf"; // Global appearance config. static constexpr char kAppearanceConfig[] = "appearance.conf"; explicit ConfigHelper(const QString& configDir); ~ConfigHelper() = default; // Gets the appearance config file path. QString appearanceConfigPath() const { return configDir_.filePath(kAppearanceConfig); } static QString wallpaperConfigKey(std::string_view desktopId, int screen) { // Screen is 0-based. return QString("wallpaper") + QString::fromStdString(std::string(desktopId)) + ((screen == 0) ? "" : (QString("_") + QString::number(screen + 1))); } // Finds the configs of all existing docks. // Returns a list of dock config paths. std::vector findAllDockConfigs() const; // Finds the next available configs for a new dock. QString findNextDockConfig() const; private: // Gets the config file name of a dock. static QString dockConfigFile(int fileId) { return QString("panel_") + QString::number(fileId) + ".conf"; } // Gets the config file path of a dock. QString dockConfigPath(int fileId) const { return configDir_.filePath(dockConfigFile(fileId)); } QString dockConfigPath(QString configFile) const { return configDir_.filePath(configFile); } QDir configDir_; friend class MultiDockModelTest; }; } // namespace crystaldock #endif // CRYSTALDOCK_CONFIG_HELPER_H_ dangvd-crystal-dock-7a34e96/src/model/launcher_config.cc000066400000000000000000000025761512233520000232450ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "launcher_config.h" #include #include "utils/desktop_file.h" namespace crystaldock { LauncherConfig::LauncherConfig(const QString& desktopFile) { DesktopFile entry(desktopFile); appId = QFileInfo(desktopFile).completeBaseName(); name = entry.name(); icon = entry.icon(); command = filterFieldCodes(entry.exec()); } void LauncherConfig::saveToFile(const QString &filePath) const { DesktopFile desktopFile; desktopFile.setName(name); desktopFile.setIcon(icon); desktopFile.setExec(command); desktopFile.setType("Application"); desktopFile.write(filePath + "/" + appId + ".desktop"); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/model/launcher_config.h000066400000000000000000000026361512233520000231040ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_LAUNCHER_CONFIG_H_ #define CRYSTALDOCK_LAUNCHER_CONFIG_H_ #include #include namespace crystaldock { struct LauncherConfig { QString appId; QString name; QString icon; QString command; LauncherConfig() = default; LauncherConfig(const QString& appId2, const QString& name2, const QString& icon2, const QString& command2) : appId(appId2), name(name2), icon(icon2), command(command2) {} LauncherConfig(const QString& desktopFile); // Saves to file in desktop file format. void saveToFile(const QString& filePath) const; }; } // namespace crystaldock #endif // CRYSTALDOCK_LAUNCHER_CONFIG_H_ dangvd-crystal-dock-7a34e96/src/model/multi_dock_model.cc000066400000000000000000000152371512233520000234270ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "multi_dock_model.h" #include #include #include #include "display/window_system.h" namespace crystaldock { MultiDockModel::MultiDockModel(const QString& configDir) : configHelper_(configDir), appearanceConfig_(configHelper_.appearanceConfigPath(), QSettings::IniFormat), desktopEnv_(DesktopEnv::getDesktopEnv()) { loadDocks(); connect(&applicationMenuConfig_, SIGNAL(configChanged()), this, SIGNAL(applicationMenuConfigChanged())); if (maxIconSize() < minIconSize()) { setMaxIconSize(minIconSize()); } if (firstRunWindowCountIndicator()) { setActiveIndicatorColor(kDefaultActiveIndicatorColor); setInactiveIndicatorColor(kDefaultInactiveIndicatorColor); } } void MultiDockModel::loadDocks() { // Dock ID starts from 1. int dockId = 1; dockConfigs_.clear(); for (const auto& configPath : configHelper_.findAllDockConfigs()) { dockConfigs_[dockId] = std::make_tuple( configPath, std::make_unique(configPath, QSettings::IniFormat)); if (screen(dockId) < static_cast(WindowSystem::screens().size())) { ++dockId; } else { // Invalid screen. dockConfigs_.erase(dockId); } } nextDockId_ = dockId; maybeAddDockForMultiScreen(); } void MultiDockModel::addDock(PanelPosition position, int screen, PanelVisibility visibility, bool showApplicationMenu, bool showPager, bool showTaskManager, bool showTrash, bool showWifiManager, bool showVolumeControl, bool showBatteryIndicator, bool showKeyboardLayout, bool showVersionChecker, bool showClock) { auto configPath = configHelper_.findNextDockConfig(); auto dockId = addDock(configPath, position, screen, visibility); setLaunchers(dockId, defaultLaunchers()); setShowApplicationMenu(dockId, showApplicationMenu); setShowPager(dockId, showPager); setShowTaskManager(dockId, showTaskManager); setShowTrash(dockId, showTrash); setShowWifiManager(dockId, showWifiManager); setShowVolumeControl(dockId, showVolumeControl); setShowBatteryIndicator(dockId, showBatteryIndicator); setShowKeyboardLayout(dockId, showKeyboardLayout); setShowVersionChecker(dockId, showVersionChecker); setShowClock(dockId, showClock); emit dockAdded(dockId); if (dockCount() == 1) { setMinIconSize(kDefaultMinSize); setMaxIconSize(kDefaultMaxSize); setSpacingFactor(kDefaultSpacingFactor); QColor color(kDefaultBackgroundColor); color.setAlphaF(kDefaultBackgroundAlpha); setBackgroundColor(color); setBorderColor(QColor(kDefaultBorderColor)); setTooltipFontSize(kDefaultTooltipFontSize); setApplicationMenuName(kDefaultApplicationMenuName); setApplicationMenuFontSize(kDefaultApplicationMenuFontSize); setApplicationMenuBackgroundAlpha(kDefaultApplicationMenuBackgroundAlpha); setUse24HourClock(kDefaultUse24HourClock); setClockFontScaleFactor(kDefaultClockFontScaleFactor); syncAppearanceConfig(); } syncDockConfig(dockId); } int MultiDockModel::addDock(const QString& configPath, PanelPosition position, int screen, PanelVisibility visibility) { const auto dockId = nextDockId_; ++nextDockId_; dockConfigs_[dockId] = std::make_tuple( configPath, std::make_unique(configPath, QSettings::IniFormat)); setPanelPosition(dockId, position); setScreen(dockId, screen); setVisibility(dockId, visibility); return dockId; } void MultiDockModel::cloneDock(int srcDockId, PanelPosition position, int screen) { auto configPath = configHelper_.findNextDockConfig(); // Clone the dock config. QFile::copy(dockConfigPath(srcDockId), configPath); auto dockId = addDock(configPath, position, screen, visibility(srcDockId)); emit dockAdded(dockId); syncDockConfig(dockId); } void MultiDockModel::removeDock(int dockId) { QFile::remove(dockConfigPath(dockId)); dockConfigs_.erase(dockId); // No need to emit a signal here. } void MultiDockModel::maybeAddDockForMultiScreen() { const auto screenCount = static_cast(WindowSystem::screens().size()); if (screenCount > 1 && dockCount() == 1 && firstRunMultiScreen()) { const auto dockId = dockConfigs_.cbegin()->first; const auto dockPosition = panelPosition(dockId); const auto dockScreen = screen(dockId); for (int screen = 0; screen < screenCount; ++screen) { if (screen != dockScreen) { cloneDock(dockId, dockPosition, screen); } } } } bool MultiDockModel::hasPager() const { for (const auto& dock : dockConfigs_) { if (showPager(dock.first)) { return true; } } return false; } const std::vector MultiDockModel::launcherConfigs(int dockId) const { std::vector entries; for (const auto& appId : launchers(dockId)) { if (appId == kSeparatorId) { entries.push_back(LauncherConfig(kSeparatorId, "", "", "")); continue; } if (appId == kLauncherSeparatorId) { entries.push_back(LauncherConfig(kLauncherSeparatorId, "", "", "")); continue; } if (appId == kShowDesktopId) { entries.push_back(LauncherConfig(kShowDesktopId, kShowDesktopName, kShowDesktopIcon, "")); continue; } const auto* entry = applicationMenuConfig_.findApplication(appId.toStdString()); if (entry != nullptr) { entries.push_back(LauncherConfig(entry->appId, entry->name, entry->icon, entry->command)); } } return entries; } QStringList MultiDockModel::defaultLaunchers() { QStringList launchers; const auto desktopEnvItems = desktopEnv_->getDefaultLaunchers(); launchers.reserve(desktopEnvItems.size()); for (const auto& appId : desktopEnvItems) { launchers.append(appId); } return launchers; } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/model/multi_dock_model.h000066400000000000000000000776361512233520000233040ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_MULTI_DOCK_MODEL_H_ #define CRYSTALDOCK_MULTI_DOCK_MODEL_H_ #include #include #include #include #include #include #include #include #include #include "application_menu_config.h" #include "config_helper.h" #include "launcher_config.h" #include namespace crystaldock { enum class PanelPosition { Top, Bottom, Left, Right }; enum class PanelVisibility { AlwaysVisible, AutoHide, AlwaysOnTop, IntelligentAutoHide }; // Glass 3D style only makes the bottom dock really 3D. For left/right/top docks, they will look // more like "Glass 2D" for aesthetic reasons. enum class PanelStyle { Glass3D_Floating, Glass3D_NonFloating, Flat2D_Floating, Flat2D_NonFloating, Metal2D_Floating, Metal2D_NonFloating, Glass2D_Floating, Glass2D_NonFloating }; constexpr int kDefaultMinSize = 48; constexpr int kDefaultMaxSize = 128; constexpr float kDefaultSpacingFactor = 0.5; constexpr bool kDefaultShowTooltip = true; constexpr int kDefaultTooltipFontSize = 24; constexpr float kDefaultBackgroundAlpha = 0.42; constexpr float kDefaultBackgroundAlphaMetal2D = 0.68; constexpr char kDefaultBackgroundColor[] = "#638abd"; constexpr char kDefaultBackgroundColor2D[] = "#86baff"; constexpr char kDefaultBackgroundColorMetal2D[] = "#7381a6"; constexpr char kDefaultBorderColor[] = "#b1c4de"; constexpr char kDefaultBorderColorMetal2D[] = "#99addd"; constexpr char kDefaultActiveIndicatorColor[] = "darkorange"; constexpr char kDefaultActiveIndicatorColor2D[] = "#ffbf00"; constexpr char kDefaultActiveIndicatorColorMetal2D[] = "#ffbf00"; constexpr char kDefaultInactiveIndicatorColor[] = "darkcyan"; constexpr char kDefaultInactiveIndicatorColor2D[] = "cyan"; constexpr char kDefaultInactiveIndicatorColorMetal2D[] = "cyan"; constexpr int kDefaultFloatingMargin = 6; constexpr bool kDefaultBouncingLauncherIcon = true; constexpr int kDefaultZoomingAnimationSpeed = 16; constexpr float kLargeClockFontScaleFactor = 1.0; constexpr float kMediumClockFontScaleFactor = 0.8; constexpr float kSmallClockFontScaleFactor = 0.6; constexpr PanelVisibility kDefaultVisibility = PanelVisibility::AlwaysVisible; constexpr bool kDefaultAutoHide = false; constexpr bool kDefaultShowApplicationMenu = true; constexpr bool kDefaultShowPager = false; constexpr bool kDefaultShowTaskManager = true; constexpr bool kDefaultShowTrash = true; constexpr bool kDefaultShowWifiManager = true; constexpr bool kDefaultShowVolumeControl = true; constexpr bool kDefaultShowBatteryIndicator = false; constexpr bool kDefaultShowKeyboardLayout = true; constexpr bool kDefaultShowVersionChecker = true; constexpr bool kDefaultShowClock = true; constexpr int kDefaultVolumeScrollStep = 2; constexpr PanelStyle kDefaultPanelStyle = PanelStyle::Glass3D_Floating; constexpr char kDefaultApplicationMenuName[] = "Applications"; constexpr int kDefaultApplicationMenuIconSize = 40; constexpr int kDefaultApplicationMenuFontSize = 14; constexpr float kDefaultApplicationMenuBackgroundAlpha = 0.8; constexpr bool kDefaultShowDesktopNumber = true; constexpr bool kDefaultCurrentDesktopTasksOnly = false; constexpr bool kDefaultCurrentScreenTasksOnly = true; constexpr bool kDefaultGroupTasksByApplication = true; constexpr bool kDefaultUse24HourClock = true; constexpr float kDefaultClockFontScaleFactor = kLargeClockFontScaleFactor; constexpr char kSeparatorId[] = "separator"; constexpr char kLauncherSeparatorId[] = "launcher-separator"; constexpr char kLockScreenId[] = "lock-screen"; constexpr char kShowDesktopId[] = "show-desktop"; constexpr char kShowDesktopName[] = "Show Desktop"; constexpr char kShowDesktopIcon[] = "user-desktop"; constexpr char kLogOutId[] = "log-out"; // The model. class MultiDockModel : public QObject { Q_OBJECT public: MultiDockModel(const QString& configDir); ~MultiDockModel() = default; MultiDockModel(const MultiDockModel&) = delete; MultiDockModel& operator=(const MultiDockModel&) = delete; // Returns the number of docks. int dockCount() const { return dockConfigs_.size(); } // Adds a new dock in the specified position and screen. void addDock(PanelPosition position, int screen, PanelVisibility visibility, bool showApplicationMenu, bool showPager, bool showTaskManager, bool showTrash, bool showWifiManager, bool showVolumeControl, bool showBatteryIndicator, bool showKeyboardLayout, bool showVersionChecker, bool showClock); // Clones an existing dock in the specified position and screen. void cloneDock(int srcDockId, PanelPosition position, int screen); // Removes a dock. void removeDock(int dockId); void maybeAddDockForMultiScreen(); int minIconSize() const { return appearanceProperty(kGeneralCategory, kMinimumIconSize, kDefaultMinSize); } void setMinIconSize(int value) { if (value > maxIconSize()) { setMaxIconSize(value); } setAppearanceProperty(kGeneralCategory, kMinimumIconSize, value); } int maxIconSize() const { return appearanceProperty(kGeneralCategory, kMaximumIconSize, kDefaultMaxSize); } void setMaxIconSize(int value) { if (value < minIconSize()) { setMinIconSize(value); } setAppearanceProperty(kGeneralCategory, kMaximumIconSize, value); } float spacingFactor() const { return appearanceProperty(kGeneralCategory, kSpacingFactor, QString::number(kDefaultSpacingFactor)).toFloat(); } // Converts float to string to make the entry in the config file human-readable. void setSpacingFactor(float value) { setAppearanceProperty(kGeneralCategory, kSpacingFactor, QString::number(value)); } QColor backgroundColor() const { QColor defaultBackgroundColor(kDefaultBackgroundColor); defaultBackgroundColor.setAlphaF(kDefaultBackgroundAlpha); return QColor(appearanceProperty(kGeneralCategory, kBackgroundColor, defaultBackgroundColor.name(QColor::HexArgb))); } void setBackgroundColor(const QColor& value) { setAppearanceProperty(kGeneralCategory, kBackgroundColor, value.name(QColor::HexArgb)); } QColor backgroundColor2D() const { QColor defaultBackgroundColor2D(kDefaultBackgroundColor2D); defaultBackgroundColor2D.setAlphaF(kDefaultBackgroundAlpha); return QColor(appearanceProperty(kGeneralCategory, kBackgroundColor2D, defaultBackgroundColor2D.name(QColor::HexArgb))); } void setBackgroundColor2D(const QColor& value) { setAppearanceProperty(kGeneralCategory, kBackgroundColor2D, value.name(QColor::HexArgb)); } QColor backgroundColorMetal2D() const { QColor defaultBackgroundColorMetal2D(kDefaultBackgroundColorMetal2D); defaultBackgroundColorMetal2D.setAlphaF(kDefaultBackgroundAlphaMetal2D); return QColor(appearanceProperty(kGeneralCategory, kBackgroundColorMetal2D, defaultBackgroundColorMetal2D.name(QColor::HexArgb))); } void setBackgroundColorMetal2D(const QColor& value) { setAppearanceProperty(kGeneralCategory, kBackgroundColorMetal2D, value.name(QColor::HexArgb)); } QColor borderColor() const { return QColor(appearanceProperty(kGeneralCategory, kBorderColor, QString(kDefaultBorderColor))); } void setBorderColor(const QColor& value) { setAppearanceProperty(kGeneralCategory, kBorderColor, value.name(QColor::HexRgb)); } QColor borderColorMetal2D() const { return QColor(appearanceProperty(kGeneralCategory, kBorderColorMetal2D, QString(kDefaultBorderColorMetal2D))); } void setBorderColorMetal2D(const QColor& value) { setAppearanceProperty(kGeneralCategory, kBorderColorMetal2D, value.name(QColor::HexRgb)); } QColor activeIndicatorColor() const { return QColor(appearanceProperty(kGeneralCategory, kActiveIndicatorColor, QString(kDefaultActiveIndicatorColor))); } void setActiveIndicatorColor(const QColor& value) { setAppearanceProperty(kGeneralCategory, kActiveIndicatorColor, value.name(QColor::HexRgb)); } QColor activeIndicatorColor2D() const { return QColor(appearanceProperty(kGeneralCategory, kActiveIndicatorColor2D, QString(kDefaultActiveIndicatorColor2D))); } void setActiveIndicatorColor2D(const QColor& value) { setAppearanceProperty(kGeneralCategory, kActiveIndicatorColor2D, value.name(QColor::HexRgb)); } QColor activeIndicatorColorMetal2D() const { return QColor(appearanceProperty(kGeneralCategory, kActiveIndicatorColorMetal2D, QString(kDefaultActiveIndicatorColorMetal2D))); } void setActiveIndicatorColorMetal2D(const QColor& value) { setAppearanceProperty(kGeneralCategory, kActiveIndicatorColorMetal2D, value.name(QColor::HexRgb)); } QColor inactiveIndicatorColor() const { return QColor(appearanceProperty(kGeneralCategory, kInactiveIndicatorColor, QString(kDefaultInactiveIndicatorColor))); } void setInactiveIndicatorColor(const QColor& value) { setAppearanceProperty(kGeneralCategory, kInactiveIndicatorColor, value.name(QColor::HexRgb)); } QColor inactiveIndicatorColor2D() const { return QColor(appearanceProperty(kGeneralCategory, kInactiveIndicatorColor2D, QString(kDefaultInactiveIndicatorColor2D))); } void setInactiveIndicatorColor2D(const QColor& value) { setAppearanceProperty(kGeneralCategory, kInactiveIndicatorColor2D, value.name(QColor::HexRgb)); } QColor inactiveIndicatorColorMetal2D() const { return QColor(appearanceProperty(kGeneralCategory, kInactiveIndicatorColorMetal2D, QString(kDefaultInactiveIndicatorColorMetal2D))); } void setInactiveIndicatorColorMetal2D(const QColor& value) { setAppearanceProperty(kGeneralCategory, kInactiveIndicatorColorMetal2D, value.name(QColor::HexRgb)); } bool showTooltip() const { return appearanceProperty(kGeneralCategory, kShowTooltip, kDefaultShowTooltip); } void setShowTooltip(bool value) { setAppearanceProperty(kGeneralCategory, kShowTooltip, value); } int tooltipFontSize() const { return appearanceProperty(kGeneralCategory, kTooltipFontSize, kDefaultTooltipFontSize); } void setTooltipFontSize(int value) { setAppearanceProperty(kGeneralCategory, kTooltipFontSize, value); } PanelStyle panelStyle() { return static_cast( appearanceProperty(kGeneralCategory, kPanelStyle, static_cast(kDefaultPanelStyle))); } void setPanelStyle(PanelStyle value) { setAppearanceProperty(kGeneralCategory, kPanelStyle, static_cast(value)); } bool is3D() { return panelStyle() == PanelStyle::Glass3D_Floating || panelStyle() == PanelStyle::Glass3D_NonFloating; } bool isGlass2D() { return panelStyle() == PanelStyle::Glass2D_Floating || panelStyle() == PanelStyle::Glass2D_NonFloating; } bool isGlass() { return is3D() || isGlass2D(); } bool isFlat2D() { return panelStyle() == PanelStyle::Flat2D_Floating || panelStyle() == PanelStyle::Flat2D_NonFloating; } bool isMetal2D() { return panelStyle() == PanelStyle::Metal2D_Floating || panelStyle() == PanelStyle::Metal2D_NonFloating; } bool isFloating() { return panelStyle() == PanelStyle::Glass3D_Floating || panelStyle() == PanelStyle::Glass2D_Floating || panelStyle() == PanelStyle::Flat2D_Floating || panelStyle() == PanelStyle::Metal2D_Floating; } int floatingMargin() const { return appearanceProperty(kGeneralCategory, kFloatingMargin, kDefaultFloatingMargin); } void setFloatingMargin(int value) { setAppearanceProperty(kGeneralCategory, kFloatingMargin, value); } bool bouncingLauncherIcon() const { return appearanceProperty(kGeneralCategory, kBouncingLauncherIcon, kDefaultBouncingLauncherIcon); } void setBouncingLauncherIcon(bool value) { setAppearanceProperty(kGeneralCategory, kBouncingLauncherIcon, value); } int zoomingAnimationSpeed() const { return appearanceProperty(kGeneralCategory, kZoomingAnimationSpeed, kDefaultZoomingAnimationSpeed); } void setZoomingAnimationSpeed(int value) { setAppearanceProperty(kGeneralCategory, kZoomingAnimationSpeed, value); } bool firstRunMultiScreen() { const auto value = appearanceProperty(kGeneralCategory, kFirstRunMultiScreen, true); setAppearanceProperty(kGeneralCategory, kFirstRunMultiScreen, false); return value; } bool firstRunWindowCountIndicator() { const auto value = appearanceProperty(kGeneralCategory, kFirstRunWindowCountIndicator, true); setAppearanceProperty(kGeneralCategory, kFirstRunWindowCountIndicator, false); return value; } QString applicationMenuName() const { return appearanceProperty(kApplicationMenuCategory, kLabel, QString(kDefaultApplicationMenuName)); } void setApplicationMenuName(const QString& value) { setAppearanceProperty(kApplicationMenuCategory, kLabel, value); } QString applicationMenuIcon() const { return desktopEnv_->getApplicationMenuIcon(); } int applicationMenuIconSize() const { return appearanceProperty(kApplicationMenuCategory, kIconSize, kDefaultApplicationMenuIconSize); } void setApplicationMenuIconSize(int value) { setAppearanceProperty(kApplicationMenuCategory, kIconSize, value); } int applicationMenuFontSize() const { return appearanceProperty(kApplicationMenuCategory, kFontSize, kDefaultApplicationMenuFontSize); } void setApplicationMenuFontSize(int value) { setAppearanceProperty(kApplicationMenuCategory, kFontSize, value); } float applicationMenuBackgroundAlpha() const { return appearanceProperty(kApplicationMenuCategory, kBackgroundAlpha, QString::number(kDefaultApplicationMenuBackgroundAlpha)).toFloat(); } // Converts float to string to make the entry in the config file human-readable. void setApplicationMenuBackgroundAlpha(float value) { setAppearanceProperty(kApplicationMenuCategory, kBackgroundAlpha, QString::number(value)); } QString wallpaper(std::string_view desktopId, int screen) const { return appearanceProperty(kPagerCategory, ConfigHelper::wallpaperConfigKey(desktopId, screen), QString()); } void setWallpaper(std::string_view desktopId, int screen, const QString& value) { setAppearanceProperty(kPagerCategory, ConfigHelper::wallpaperConfigKey(desktopId, screen), value); } // Notifies that the wallpaper for the current desktop for the specified // screen has been changed. void notifyWallpaperChanged(int screen) { emit wallpaperChanged(screen); } bool showDesktopNumber() const { return appearanceProperty(kPagerCategory, kShowDesktopNumber, kDefaultShowDesktopNumber); } void setShowDesktopNumber(bool value) { setAppearanceProperty(kPagerCategory, kShowDesktopNumber, value); } bool currentDesktopTasksOnly() const { return appearanceProperty(kTaskManagerCategory, kCurrentDesktopTasksOnly, kDefaultCurrentDesktopTasksOnly); } void setCurrentDesktopTasksOnly(bool value) { setAppearanceProperty(kTaskManagerCategory, kCurrentDesktopTasksOnly, value); } bool currentScreenTasksOnly() const { return appearanceProperty(kTaskManagerCategory, kCurrentScreenTasksOnly, kDefaultCurrentScreenTasksOnly); } void setCurrentScreenTasksOnly(bool value) { setAppearanceProperty(kTaskManagerCategory, kCurrentScreenTasksOnly, value); } bool groupTasksByApplication() const { return appearanceProperty(kTaskManagerCategory, kGroupTasksByApplication, kDefaultGroupTasksByApplication); } void setGroupTasksByApplication(bool value) { setAppearanceProperty(kTaskManagerCategory, kGroupTasksByApplication, value); } bool use24HourClock() const { return appearanceProperty(kClockCategory, kUse24HourClock, kDefaultUse24HourClock); } void setUse24HourClock(bool value) { setAppearanceProperty(kClockCategory, kUse24HourClock, value); } float clockFontScaleFactor() const { return appearanceProperty(kClockCategory, kFontScaleFactor, QString::number(kDefaultClockFontScaleFactor)).toFloat(); } // Converts float to string to make the entry in the config file human-readable. void setClockFontScaleFactor(float value) { setAppearanceProperty(kClockCategory, kFontScaleFactor, QString::number(value)); } QString clockFontFamily() const { return appearanceProperty(kClockCategory, kClockFontFamily, QString()); } void setClockFontFamily(const QString& value) { setAppearanceProperty(kClockCategory, kClockFontFamily, value); } void saveAppearanceConfig(bool repaintOnly = false) { syncAppearanceConfig(); if (repaintOnly) { emit appearanceOutdated(); } else { emit appearanceChanged(); } } PanelPosition panelPosition(int dockId) const { return static_cast(dockProperty( dockId, kGeneralCategory, kPosition, static_cast(PanelPosition::Bottom))); } void setPanelPosition(int dockId, PanelPosition value) { setDockProperty(dockId, kGeneralCategory, kPosition, static_cast(value)); } int screen(int dockId) const { return dockProperty(dockId, kGeneralCategory, kScreen, 0); } void setScreen(int dockId, int value) { setDockProperty(dockId, kGeneralCategory, kScreen, value); } PanelVisibility visibility(int dockId) const { if (autoHide(dockId)) { // for backward compatibility. return PanelVisibility::AutoHide; } return static_cast(dockProperty( dockId, kGeneralCategory, kVisibility, static_cast(kDefaultVisibility))); } void setVisibility(int dockId, PanelVisibility value) { setDockProperty(dockId, kGeneralCategory, kVisibility, static_cast(value)); // For backward compatibility. setAutoHide(dockId, value == PanelVisibility::AutoHide); } bool autoHide(int dockId) const { return dockProperty(dockId, kGeneralCategory, kAutoHide, kDefaultAutoHide); } void setAutoHide(int dockId, bool value) { setDockProperty(dockId, kGeneralCategory, kAutoHide, value); } bool showApplicationMenu(int dockId) const { return dockProperty(dockId, kGeneralCategory, kShowApplicationMenu, kDefaultShowApplicationMenu); } void setShowApplicationMenu(int dockId, bool value) { setDockProperty(dockId, kGeneralCategory, kShowApplicationMenu, value); } bool showPager(int dockId) const { return dockProperty(dockId, kGeneralCategory, kShowPager, kDefaultShowPager); } void setShowPager(int dockId, bool value) { setDockProperty(dockId, kGeneralCategory, kShowPager, value); } bool showTaskManager(int dockId) const { return dockProperty(dockId, kGeneralCategory, kShowTaskManager, kDefaultShowTaskManager); } void setShowTaskManager(int dockId, bool value) { setDockProperty(dockId, kGeneralCategory, kShowTaskManager, value); } bool showTrash(int dockId) const { return dockProperty(dockId, kGeneralCategory, kShowTrash, kDefaultShowTrash); } void setShowTrash(int dockId, bool value) { setDockProperty(dockId, kGeneralCategory, kShowTrash, value); } bool showWifiManager(int dockId) const { return dockProperty(dockId, kGeneralCategory, kShowWifiManager, kDefaultShowWifiManager); } void setShowWifiManager(int dockId, bool value) { setDockProperty(dockId, kGeneralCategory, kShowWifiManager, value); } bool showVolumeControl(int dockId) const { return dockProperty(dockId, kGeneralCategory, kShowVolumeControl, kDefaultShowVolumeControl); } void setShowVolumeControl(int dockId, bool value) { setDockProperty(dockId, kGeneralCategory, kShowVolumeControl, value); } bool showBatteryIndicator(int dockId) const { return dockProperty(dockId, kGeneralCategory, kShowBatteryIndicator, kDefaultShowBatteryIndicator); } void setShowBatteryIndicator(int dockId, bool value) { setDockProperty(dockId, kGeneralCategory, kShowBatteryIndicator, value); } bool showKeyboardLayout(int dockId) const { return dockProperty(dockId, kGeneralCategory, kShowKeyboardLayout, kDefaultShowKeyboardLayout); } void setShowKeyboardLayout(int dockId, bool value) { setDockProperty(dockId, kGeneralCategory, kShowKeyboardLayout, value); } bool showVersionChecker(int dockId) const { return dockProperty(dockId, kGeneralCategory, kShowVersionChecker, kDefaultShowVersionChecker); } void setShowVersionChecker(int dockId, bool value) { setDockProperty(dockId, kGeneralCategory, kShowVersionChecker, value); } bool showClock(int dockId) const { return dockProperty(dockId, kGeneralCategory, kShowClock, kDefaultShowClock); } void setShowClock(int dockId, bool value) { setDockProperty(dockId, kGeneralCategory, kShowClock, value); } int volumeScrollStep() const { return appearanceProperty(kVolumeControlCategory, kVolumeScrollStep, kDefaultVolumeScrollStep); } void setVolumeScrollStep(int value) { setAppearanceProperty(kVolumeControlCategory, kVolumeScrollStep, value); } QString activeKeyboardLayout() const { return appearanceProperty(kKeyboardLayoutCategory, kActiveKeyboardLayout, QString()); } void setActiveKeyboardLayout(QString value) { return setAppearanceProperty(kKeyboardLayoutCategory, kActiveKeyboardLayout, value); } QStringList userKeyboardLayouts() const { return appearanceProperty(kKeyboardLayoutCategory, kUserKeyboardLayouts, QString()) .split(";", Qt::SkipEmptyParts); } void setUserKeyboardLayouts(QStringList value) { return setAppearanceProperty(kKeyboardLayoutCategory, kUserKeyboardLayouts, value.join(";")); } QStringList launchers(int dockId) const { return dockProperty(dockId, kGeneralCategory, kLaunchers, QString()) .split(";", Qt::SkipEmptyParts); } void setLaunchers(int dockId, QStringList value) { setDockProperty(dockId, kGeneralCategory, kLaunchers, value.join(";")); } void saveDockConfig(int dockId) { syncDockConfig(dockId); emit dockLaunchersChanged(dockId); } const std::vector launcherConfigs(int dockId) const; void addLauncher(int dockId, const LauncherConfig& launcher) { auto entries = launchers(dockId); unsigned int i = 0; for (; i < entries.size() && entries[i] != kSeparatorId; ++i) {} entries.insert(i, launcher.appId); setLaunchers(dockId, entries); syncDockConfig(dockId); } void removeLauncher(int dockId, const QString& appId) { auto entries = launchers(dockId); for (unsigned i = 0; i < entries.size(); ++i) { if (entries[i] == appId) { entries.remove(i); setLaunchers(dockId, entries); syncDockConfig(dockId); return; } } } // Whether any dock has a pager. bool hasPager() const; const std::vector& applicationMenuCategories() const { return applicationMenuConfig_.categories(); } const std::vector& applicationMenuSystemCategories() const { return applicationMenuConfig_.systemCategories(); } const ApplicationEntry* findApplication(const std::string& appId) const { return applicationMenuConfig_.tryMatchingApplicationId(appId); } bool isAppMenuEntry(const std::string& appId) const { return applicationMenuConfig_.isAppMenuEntry(appId); } const std::vector searchApplications( const QString& text, unsigned int maxNumResults) const { return applicationMenuConfig_.searchApplications(text, maxNumResults); } signals: // Minor appearance changes that require view update (repaint). void appearanceOutdated(); // Major appearance changes that require view reload. void appearanceChanged(); void dockAdded(int dockId); void dockLaunchersChanged(int dockId); // Wallpaper for the current desktop for screen has been changed. // Will require calling Plasma D-Bus to update the wallpaper. void wallpaperChanged(int screen); void applicationMenuConfigChanged(); private: // Dock config's categories/properties. static constexpr char kGeneralCategory[] = ""; static constexpr char kAutoHide[] = "autoHide"; static constexpr char kVisibility[] = "visibility"; static constexpr char kPosition[] = "position"; static constexpr char kScreen[] = "screen"; static constexpr char kShowApplicationMenu[] = "showApplicationMenu"; static constexpr char kShowPager[] = "showPager"; static constexpr char kShowTaskManager[] = "showTaskManager"; static constexpr char kShowTrash[] = "showTrash"; static constexpr char kShowWifiManager[] = "showWifiManager"; static constexpr char kShowVolumeControl[] = "showVolumeControl"; static constexpr char kShowBatteryIndicator[] = "showBatteryIndicator"; static constexpr char kShowKeyboardLayout[] = "showKeyboardLayout"; static constexpr char kShowVersionChecker[] = "showVersionChecker"; static constexpr char kShowClock[] = "showClock"; static constexpr char kLaunchers[] = "launchers"; // Global appearance config's categories/properties. // General category. static constexpr char kBackgroundColor[] = "backgroundColor"; static constexpr char kBackgroundColor2D[] = "backgroundColor2D"; static constexpr char kBackgroundColorMetal2D[] = "backgroundColorMetal2D"; static constexpr char kBorderColor[] = "borderColor"; static constexpr char kBorderColorMetal2D[] = "borderColorMetal2D"; static constexpr char kActiveIndicatorColor[] = "activeIndicatorColor"; static constexpr char kActiveIndicatorColor2D[] = "activeIndicatorColor2D"; static constexpr char kActiveIndicatorColorMetal2D[] = "activeIndicatorColorMetal2D"; static constexpr char kInactiveIndicatorColor[] = "inactiveIndicatorColor"; static constexpr char kInactiveIndicatorColor2D[] = "inactiveIndicatorColor2D"; static constexpr char kInactiveIndicatorColorMetal2D[] = "inactiveIndicatorColorMetal2D"; static constexpr char kMaximumIconSize[] = "maximumIconSize"; static constexpr char kMinimumIconSize[] = "minimumIconSize"; static constexpr char kSpacingFactor[] = "spacingFactor"; static constexpr char kShowTooltip[] = "showTooltip"; static constexpr char kTooltipFontSize[] = "tooltipFontSize"; static constexpr char kPanelStyle[] = "panelStyle"; static constexpr char kFloatingMargin[] = "floatingMargin"; static constexpr char kBouncingLauncherIcon[] = "bouncingLauncherIcon"; static constexpr char kZoomingAnimationSpeed[] = "zoomingAnimationSpeed"; static constexpr char kFirstRunMultiScreen[] = "firstRunMultiScreen"; static constexpr char kFirstRunWindowCountIndicator[] = "firstRunWindowCountIndicator"; static constexpr char kApplicationMenuCategory[] = "Application Menu"; static constexpr char kLabel[] = "label"; static constexpr char kIconSize[] = "iconSize"; static constexpr char kFontSize[] = "fontSize"; static constexpr char kBackgroundAlpha[] = "backgroundAlpha"; static constexpr char kPagerCategory[] = "Pager"; static constexpr char kWallpaper[] = "wallpaper"; static constexpr char kShowDesktopNumber[] = "showDesktopNumber"; static constexpr char kTaskManagerCategory[] = "TaskManager"; static constexpr char kCurrentDesktopTasksOnly[] = "currentDesktopTasksOnly"; static constexpr char kCurrentScreenTasksOnly[] = "currentScreenTasksOnly"; static constexpr char kGroupTasksByApplication[] = "groupTasksByApplication"; static constexpr char kVolumeControlCategory[] = "VolumeControl"; static constexpr char kVolumeScrollStep[] = "volumeScrollStep"; static constexpr char kKeyboardLayoutCategory[] = "KeyboardLayout"; // The active keyboard layout (the engine name). static constexpr char kActiveKeyboardLayout[] = "activeKeyboardLayout"; // The list of user-selected keyboard layouts (the engine names) for fast switching. static constexpr char kUserKeyboardLayouts[] = "userKeyboardLayouts"; static constexpr char kClockCategory[] = "Clock"; static constexpr char kUse24HourClock[] = "use24HourClock"; static constexpr char kFontScaleFactor[] = "fontScaleFactor"; static constexpr char kClockFontFamily[] = "clockFontFamily"; template T appearanceProperty(QString category, QString name, T defaultValue) const { return category.isEmpty() ? appearanceConfig_.value(name, defaultValue).template value() : appearanceConfig_.value(category + '/' + name, defaultValue).template value(); } template void setAppearanceProperty(QString category, QString name, T value) { if (category.isEmpty()) { appearanceConfig_.setValue(name, value); return; } appearanceConfig_.beginGroup(category); appearanceConfig_.setValue(name, value); appearanceConfig_.endGroup(); } template T dockProperty(int dockId, QString category, QString name, T defaultValue) const { return category.isEmpty() ? dockConfig(dockId)->value(name, defaultValue).template value() : dockConfig(dockId)->value(category + '/' + name, defaultValue).template value(); } template void setDockProperty(int dockId, QString category, QString name, T value) { auto* config = dockConfig(dockId); if (category.isEmpty()) { config->setValue(name, value); return; } config->beginGroup(category); config->setValue(name, value); config->endGroup(); } QString dockConfigPath(int dockId) const { return std::get<0>(dockConfigs_.at(dockId)); } const QSettings* dockConfig(int dockId) const { return std::get<1>(dockConfigs_.at(dockId)).get(); } QSettings* dockConfig(int dockId) { return std::get<1>(dockConfigs_[dockId]).get(); } QStringList defaultLaunchers(); void loadDocks(); int addDock(const QString& configPath, PanelPosition position, int screen, PanelVisibility visibility); void syncAppearanceConfig() { appearanceConfig_.sync(); } void syncDockConfig(int dockId) { dockConfig(dockId)->sync(); } // Helper(s). ConfigHelper configHelper_; // Model data. // Appearance config. QSettings appearanceConfig_; // Dock configs, as map from dockIds to tuples of: // (dock config file path, // dock config) std::unordered_map>> dockConfigs_; // ID for the next dock. int nextDockId_; ApplicationMenuConfig applicationMenuConfig_; DesktopEnv* desktopEnv_; }; } // namespace crystaldock #endif // CRYSTALDOCK_MULTI_DOCK_MODEL_H_ dangvd-crystal-dock-7a34e96/src/utils/000077500000000000000000000000001512233520000176365ustar00rootroot00000000000000dangvd-crystal-dock-7a34e96/src/utils/command_utils.h000066400000000000000000000043771512233520000226600ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_COMMAND_UTILS_H_ #define CRYSTALDOCK_COMMAND_UTILS_H_ #include #include #include #include namespace crystaldock { // TODO: write unit tests for this file. inline QString filterFieldCodes(const QString& command) { QString filtered = command.contains('%') ? command.left(command.indexOf('%') - 1) : command; if (filtered.startsWith("env")) { int i = filtered.lastIndexOf("="); filtered = filtered.mid(filtered.indexOf(" ", i) + 1); } return filtered; } // Given "/usr/bin/command-1.0.AppImage -a -b", returns "command" inline QString getShortCommand(const QString& command) { QString shortCommand = command.contains(' ') ? command.left(command.indexOf(' ')) : command; shortCommand = shortCommand.mid(shortCommand.lastIndexOf('/') + 1); if (shortCommand.contains('-')) { shortCommand = shortCommand.left(shortCommand.indexOf('-')); } if (shortCommand.contains('.')) { shortCommand = shortCommand.left(shortCommand.indexOf('.')); } return shortCommand; } // Returns a command in the list that exists in the system. inline QString commandExists(std::vector commands) { QStringList paths = qEnvironmentVariable("PATH").split(":", Qt::SkipEmptyParts); for (const auto& path : paths) { QDir dir(path); for (const auto& command : commands) { if (dir.exists(command)) { return command; } } } return QString(); } } // namespace crystaldock #endif // CRYSTALDOCK_COMMAND_UTILS_H_ dangvd-crystal-dock-7a34e96/src/utils/desktop_file.cc000066400000000000000000000042341512233520000226200ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "desktop_file.h" #include #include #include #include namespace crystaldock { DesktopFile::DesktopFile(const QString& file) { QFile inputFile(file); if (inputFile.open(QIODevice::ReadOnly)) { appId_ = QFileInfo(file).completeBaseName().toLower(); QTextStream input(&inputFile); bool parsing = false; while (!input.atEnd()) { QString line = input.readLine().trimmed(); if (!parsing) { if (line == "[Desktop Entry]") { parsing = true; } } else { int index = line.indexOf('='); if (index >= 0 && index < line.length() - 1) { values_[line.left(index)] = line.mid(index + 1); } else if (line.startsWith("[")) { // start of a new section. break; } } } } } bool DesktopFile::write(const QString &file) { QFile outputFile(file); if (outputFile.open(QIODevice::WriteOnly)) { QTextStream output(&outputFile); output << "[Desktop Entry]\n"; for (const auto& key : values_.keys()) { output << key << "=" << values_[key] << "\n"; } return true; } return false; } bool DesktopFile::showOnDesktop(const QString& desktop) const { if (!onlyShowIn().empty() && !onlyShowIn().contains(desktop)) { return false; } if (!notShowIn().empty() && notShowIn().contains(desktop)) { return false; } return true; } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/utils/desktop_file.h000066400000000000000000000057551512233520000224730ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_DESKTOP_FILE_H #define CRYSTALDOCK_DESKTOP_FILE_H #include #include #include namespace crystaldock { // Follows Desktop Entry Specification: // https://specifications.freedesktop.org/desktop-entry-spec/latest/index.html class DesktopFile { public: DesktopFile() {} DesktopFile(const QString &file); bool write(const QString &file); QString appId() const { return appId_; } QString name() const { return values_["Name"]; } void setName(const QString& name) { values_["Name"] = name; } QString wmClass() const { return values_["StartupWMClass"]; } void setWMClass(const QString& wmClass) { values_["StartupWMClass"] = wmClass; } QString genericName() const { return values_["GenericName"]; } void setGenericName(const QString& genericName) { values_["GenericName"] = genericName; } QString icon() const { return values_["Icon"]; } void setIcon(const QString& icon) { values_["Icon"] = icon; } QString exec() const { return values_["Exec"]; } void setExec(const QString& exec) { values_["Exec"] = exec; } QString type() const { return values_["Type"]; } void setType(const QString& type) { values_["Type"] = type; } QStringList categories() const { return values_["Categories"].split(";", Qt::SkipEmptyParts); } void setCategories(const QString& categories) { values_["Categories"] = categories; } QStringList onlyShowIn() const { return values_["OnlyShowIn"].split(";", Qt::SkipEmptyParts); } void setOnlyShowIn(const QString& desktops) { values_["OnlyShowIn"] = desktops; } QStringList notShowIn() const { return values_["NotShowIn"].split(";", Qt::SkipEmptyParts); } void setNotShowIn(const QString& desktops) { values_["NotShowIn"] = desktops; } bool noDisplay() const { return values_["NoDisplay"].toLower() == "true"; } void setNoDisplay(bool value) { values_["NoDisplay"] = value ? "true" : "false"; } bool hidden() const { return values_["Hidden"].toLower() == "true"; } void setHidden(bool value) { values_["Hidden"] = value ? "true" : "false"; } // Should we show this entry on this desktop? bool showOnDesktop(const QString& desktop) const; private: QString appId_; QMap values_; }; } // namespace crystaldock #endif // CRYSTALDOCK_DESKTOP_FILE_H dangvd-crystal-dock-7a34e96/src/utils/draw_utils.h000066400000000000000000000203011512233520000221600ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_DRAW_UTILS_H_ #define CRYSTALDOCK_DRAW_UTILS_H_ #include #include #include #include #include #include namespace crystaldock { inline void drawBorderedText(int x, int y, const QString& text, int borderWidth, QColor borderColor, QColor textColor, QPainter* painter, bool simplified = false) { painter->setPen(borderColor); const int delta = simplified ? 2 * borderWidth : 1; for (int i = -borderWidth; i <= borderWidth; i += delta) { for (int j = -borderWidth; j <= borderWidth; j += delta) { if (i != 0 || j != 0) { painter->drawText(x + i, y + j, text); } } } painter->setPen(textColor); painter->drawText(x, y, text); } inline void drawBorderedText(int x, int y, int width, int height, int flags, const QString& text, int borderWidth, QColor borderColor, QColor textColor, QPainter* painter, bool simplified = false) { painter->setPen(borderColor); const int delta = simplified ? 2 * borderWidth : 1; for (int i = -borderWidth; i <= borderWidth; i += delta) { for (int j = -borderWidth; j <= borderWidth; j += delta) { if (i != 0 || j != 0) { painter->drawText(x + i, y + j, width, height, flags, text); } } } painter->setPen(textColor); painter->drawText(x, y, width, height, flags, text); } inline void drawHighlightedIcon(QColor bgColor, int left, int top, int width, int height, int padding, int roundedRectRadius, QPainter* painter, float alpha = 0.42) { painter->setRenderHint(QPainter::Antialiasing); QColor fillColor = bgColor.lighter(500); fillColor.setAlphaF(alpha); QPainterPath path; path.addRoundedRect( QRect(left - padding, top - padding, width + 2 * padding, height + 2 * padding), roundedRectRadius, roundedRectRadius); painter->fillPath(path, QBrush(fillColor)); painter->setRenderHint(QPainter::Antialiasing, false); } inline void fillRoundedRect(int x, int y, int width, int height, int radius, bool showBorder, QColor borderColor, QColor fillColor, QPainter* painter) { painter->setRenderHint(QPainter::Antialiasing); QPainterPath border; border.addRoundedRect(x + 0.5, y + 0.5, width, height, radius, radius); painter->fillPath(border, QBrush(fillColor)); if (showBorder) { painter->setPen(borderColor); painter->drawPath(border); } painter->setRenderHint(QPainter::Antialiasing, false); } inline void draw3dDockPanel(int x, int y, int width, int height, int radius, QColor borderColor, QColor fillColor, QPainter* painter) { painter->setRenderHint(QPainter::Antialiasing); QPainterPath surface; QPolygonF polygon; polygon << QPointF(x + height / 2, y + height / 2) << QPointF(x + width - height / 2, y + height / 2) << QPointF(x + width, y + height) << QPointF(x, y + height); surface.addPolygon(polygon); surface.closeSubpath(); painter->fillPath(surface, QBrush(fillColor)); painter->setPen(borderColor); painter->drawPath(surface); QPainterPath side; QPolygonF polygon2; polygon2 << QPointF(x + height / 2, y + height / 2) << QPointF(x + height / 2, y + height / 2 + 2) << QPointF(x, y + height + 2) << QPointF(x, y + height) ; side.addPolygon(polygon2); side.closeSubpath(); QPolygonF polygon3; polygon3 << QPointF(x + width - height / 2, y + height / 2) << QPointF(x + width - height / 2, y + height / 2 + 2) << QPointF(x + width, y + height + 2) << QPointF(x + width, y + height); side.addPolygon(polygon3); side.closeSubpath(); painter->fillPath(side, QBrush(fillColor)); painter->fillRect(x + height / 2, y + height / 2, width - height, 2, QBrush(fillColor)); painter->fillRect(x, y + height, width, 3, QBrush(borderColor)); painter->setRenderHint(QPainter::Antialiasing, false); } inline void fillCircle(int x, int y, int width, int height, QColor bgColor, QPainter* painter) { QColor fillColor = bgColor; fillColor.setAlphaF(1.0); painter->setRenderHint(QPainter::Antialiasing); QPainterPath border; border.addEllipse(x + 0.5, y + 0.5, width, height); painter->fillPath(border, QBrush(fillColor)); painter->setRenderHint(QPainter::Antialiasing, false); } inline void drawIndicator(Qt::Orientation orientation, int hx, int hy, int vx, int vy, int size, int thickness, QColor baseColor, QPainter* painter) { for (int i = 0; i <= size; i++) { int brightness = 100 - (2 * i - size) * (2 * i - size) * 100 / (size * size); if (brightness < 10) { brightness = 10; } QColor color = baseColor.lighter(brightness * 16 / 10); if (orientation == Qt::Horizontal) { painter->fillRect(hx - size / 2 + i, hy, 1, thickness, color); painter->fillRect(hx - size / 2 + i, hy, 1, 1, color.darker(300)); painter->fillRect(hx - size / 2 + i, hy + thickness, 1, 1, color.darker(300)); } else { // Vertical. painter->fillRect(vx, vy - size / 2 + i, thickness, 1, color); painter->fillRect(vx, vy - size / 2 + i, 1, 1, color.darker(300)); painter->fillRect(vx + thickness, vy - size / 2 + i, 1, 1, color.darker(300)); } } } inline void drawIndicatorFlat2D(Qt::Orientation orientation, int hx, int hy, int vx, int vy, int size, QColor baseColor, QPainter* painter) { if (orientation == Qt::Horizontal) { fillCircle(hx - size / 2, hy, size, size, baseColor, painter); } else { fillCircle(vx, vy - size / 2, size, size, baseColor, painter); } } inline void drawIndicatorMetal2D(PanelPosition panelPosition, int hx, int hy, int vx, int vy, int size, QColor baseColor, QPainter* painter) { painter->setPen(baseColor); painter->setBrush(baseColor); if (panelPosition == PanelPosition::Top) { QPoint points[3] = { QPoint(hx, hy + size / 2), QPoint(hx - size / 2, hy), QPoint(hx + size / 2, hy) }; painter->drawPolygon(points, 3); } else if (panelPosition == PanelPosition::Bottom) { QPoint points[3] = { QPoint(hx, hy), QPoint(hx - size / 2, hy + size / 2), QPoint(hx + size / 2, hy + size / 2) }; painter->drawPolygon(points, 3); } else if (panelPosition == PanelPosition::Left) { QPoint points[3] = { QPoint(vx + size / 2, vy), QPoint(vx, vy - size / 2), QPoint(vx, vy + size / 2) }; painter->drawPolygon(points, 3); } else if (panelPosition == PanelPosition::Right) { QPoint points[3] = { QPoint(vx, vy), QPoint(vx + size / 2, vy - size / 2), QPoint(vx + size / 2, vy + size / 2) }; painter->drawPolygon(points, 3); } painter->setBrush(Qt::transparent); } inline void drawFallbackIcon(int left, int top, int size, QColor borderColor, QColor fillColor, QPainter* painter) { painter->setRenderHint(QPainter::Antialiasing); painter->setPen(borderColor); painter->setBrush(fillColor); painter->drawEllipse(left, top, size, size); painter->setBrush(Qt::NoBrush); painter->setPen(fillColor.lighter()); painter->drawEllipse(left + 1, top + 1, size - 2, size - 2); painter->setPen(fillColor); painter->drawEllipse(left + 2, top + 2, size - 4, size - 4); painter->setRenderHint(QPainter::Antialiasing, false); } } // namespace crystaldock #endif // CRYSTALDOCK_DRAW_UTILS_H_ dangvd-crystal-dock-7a34e96/src/utils/font_utils.h000066400000000000000000000047421512233520000222040ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_FONT_UTILS_H_ #define CRYSTALDOCK_FONT_UTILS_H_ #include #include #include #include #include #include #include namespace crystaldock { // Returns a QFont with font size adjusted automatically according to the given // width, height, reference string and scale factor. inline QFont adjustFontSize(int w, int h, const QString& referenceString, float scaleFactor, const QString& fontFamily = "") { QFont font; QFontMetrics metrics(font); const QRect& rect = metrics.tightBoundingRect(referenceString); // Scale the font size according to the size of the dock. font.setPointSize(std::min(font.pointSize() * w / rect.width(), font.pointSize() * h / rect.height())); font.setPointSize(static_cast(font.pointSize() * scaleFactor)); if (!fontFamily.isEmpty()) { font.setFamily(fontFamily); } return font; } // Gets the list of base font families, i.e. just 'Noto Sans' // instead of 'Noto Sans Bold', 'Noto Sans CJK' etc. inline std::vector getBaseFontFamilies() { std::vector baseFamilies; QFontDatabase database; const auto families = database.families(QFontDatabase::Latin); for (int i = 0; i < families.size(); ++i) { bool isBaseFont = true; const auto family = families.at(i); if (database.isSmoothlyScalable(family)) { for (int j = 0; j < families.size(); ++j) { if (family.startsWith(families[j] + ' ')) { isBaseFont = false; break; } } if (isBaseFont) { baseFamilies.push_back(family); } } } return baseFamilies; } } // namespace crystaldock #endif // CRYSTALDOCK_FONT_UTILS_H_ dangvd-crystal-dock-7a34e96/src/utils/icon_utils.h000066400000000000000000000024501512233520000221600ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2024 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_ICON_UTILS_H_ #define CRYSTALDOCK_ICON_UTILS_H_ #include #include namespace crystaldock { inline QPixmap loadIcon(const QString& iconName, int iconLoadSize) { // Normally desktop file stores icon as icon name in the icon theme. QPixmap icon = QIcon::fromTheme(iconName).pixmap(iconLoadSize); if (!icon.isNull()) { return icon; } // Snap stores icon name as path to the icon file e.g. // /snap/firefox/5134/default256.png return QPixmap(iconName); } } // namespace crystaldock #endif // CRYSTALDOCK_ICON_UTILS_H_ dangvd-crystal-dock-7a34e96/src/utils/math_utils.h000066400000000000000000000022011512233520000221530ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_MATH_UTILS_H_ #define CRYSTALDOCK_MATH_UTILS_H_ #include namespace crystaldock { inline int alphaFToTransparencyPercent(float alphaF) { return static_cast(std::round(100 * (1 - alphaF))); } inline float transparencyPercentToAlphaF(int transparencyPercent) { return 1 - transparencyPercent / 100.0; } } // namespace crystaldock #endif // CRYSTALDOCK_MATH_UTILS_H_ dangvd-crystal-dock-7a34e96/src/utils/menu_utils.h000066400000000000000000000026111512233520000221730ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2024 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_MENU_UTILS_H_ #define CRYSTALDOCK_MENU_UTILS_H_ #include #include #include #include namespace crystaldock { // A work-around for sub-menu alignment issue on Wayland by adding // empty items to the sub-menu. inline void patchMenu(unsigned int totalNumItems, int iconSize, QMenu* menu) { QPixmap pix(iconSize, iconSize); pix.fill(QColorConstants::Transparent); const QIcon icon(pix); const auto numItemsToAdd = totalNumItems - menu->actions().size(); for (unsigned int i = 0; i < numItemsToAdd; ++i) { menu->addAction(icon, ""); } } } // namespace crystaldock #endif // CRYSTALDOCK_MENU_UTILS_H_ dangvd-crystal-dock-7a34e96/src/view/000077500000000000000000000000001512233520000174505ustar00rootroot00000000000000dangvd-crystal-dock-7a34e96/src/view/add_panel_dialog.cc000066400000000000000000000203351512233520000232100ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "add_panel_dialog.h" #include "ui_add_panel_dialog.h" #include #include "battery_indicator.h" #include #include namespace crystaldock { namespace { void moveY(QWidget* widget, int deltaY) { widget->move(widget->x(), widget->y() + deltaY); } void resizeHeight(QWidget* widget, int deltaHeight) { widget->resize(widget->width(), widget->height() + deltaHeight); } } // namespace AddPanelDialog::AddPanelDialog(QWidget* parent, MultiDockModel* model, int dockId) : QDialog(parent), ui(new Ui::AddPanelDialog), model_(model), dockId_(dockId), isSingleScreen_(true) { ui->setupUi(this); setWindowFlag(Qt::Tool); // Populate screen list. const int screenCount = WindowSystem::screens().size(); for (int i = 1; i <= screenCount; ++i) { ui->screen->addItem(QString::number(i)); } ui->screen->setCurrentIndex(0); // Adjust the UI for single/multi-screen. isSingleScreen_ = (screenCount == 1); if (isSingleScreen_) { ui->screenLabel->setVisible(false); ui->screen->setVisible(false); } } AddPanelDialog::~AddPanelDialog() { delete ui; } void AddPanelDialog::setMode(Mode mode) { mode_ = mode; // reset positions of fields and size. ui->showTaskManager->move(120, 180); ui->showTrash->move(120, 220); ui->showWifiManager->move(120, 260); ui->showVolumeControl->move(120, 300); ui->showBatteryIndicator->move(120, 340); ui->showKeyboardLayout->move(120, 380); ui->showVersionChecker->move(120, 420); ui->showClock->move(120, 460); ui->styleLabel->move(60, 515); ui->style->move(240, 505); ui->positionLabel->move(60, 560); ui->position->move(240, 550); ui->screenLabel->move(60, 605); ui->screen->move(240, 595); ui->visibilityLabel->move(60, 650); ui->visibility->move(240, 640); ui->buttonBox->move(70, 710); resize(540, 770); setWindowTitle((mode_ == Mode::Add) ? QString("Add Panel") : (mode_ == Mode::Clone) ? QString("Clone Panel") : "Welcome to Crystal Dock!"); ui->headerLabel->setText((mode == Mode::Welcome) ? QString("Please set up your first panel.") : QString("Please set up your new panel.")); ui->showApplicationMenu->setChecked(mode == Mode::Welcome); ui->showPager->setChecked(false); ui->showTaskManager->setChecked(mode == Mode::Welcome); ui->showTrash->setChecked(mode == Mode::Welcome); ui->showWifiManager->setChecked(mode == Mode::Welcome); ui->showVolumeControl->setChecked(mode == Mode::Welcome); ui->showBatteryIndicator->setChecked( mode == Mode::Welcome && !BatteryIndicator::getBatteryDevice().isEmpty()); ui->showKeyboardLayout->setChecked(mode == Mode::Welcome); ui->showVersionChecker->setChecked(mode == Mode::Welcome); ui->showClock->setChecked(mode == Mode::Welcome); ui->componentsLabel->setVisible(mode != Mode::Clone); ui->showApplicationMenu->setVisible(mode != Mode::Clone); ui->showPager->setVisible(mode != Mode::Clone); ui->showTaskManager->setVisible(mode != Mode::Clone); ui->showTrash->setVisible(mode != Mode::Clone); ui->showWifiManager->setVisible(mode != Mode::Clone); ui->showVolumeControl->setVisible(mode != Mode::Clone); ui->showBatteryIndicator->setVisible(mode != Mode::Clone); ui->showKeyboardLayout->setVisible(mode != Mode::Clone); ui->showVersionChecker->setVisible(mode != Mode::Clone); ui->showClock->setVisible(mode != Mode::Clone); // See https://github.com/dangvd/crystal-dock/issues/218 ui->visibility->setCurrentText( DesktopEnv::getDesktopEnvName() == "Wayfire" ? "Intelligent Auto Hide" : "Always Visible"); if (mode != Mode::Clone && !WindowSystem::hasVirtualDesktopManager()) { ui->showPager->setChecked(false); ui->showPager->setVisible(false); constexpr int kDeltaY = -40; moveY(ui->showTaskManager, kDeltaY); moveY(ui->showTrash, kDeltaY); moveY(ui->showWifiManager, kDeltaY); moveY(ui->showVolumeControl, kDeltaY); moveY(ui->showBatteryIndicator, kDeltaY); moveY(ui->showKeyboardLayout, kDeltaY); moveY(ui->showVersionChecker, kDeltaY); moveY(ui->showClock, kDeltaY); moveY(ui->styleLabel, kDeltaY); moveY(ui->style, kDeltaY); moveY(ui->positionLabel, kDeltaY); moveY(ui->position, kDeltaY); moveY(ui->screenLabel, kDeltaY); moveY(ui->screen, kDeltaY); moveY(ui->visibilityLabel, kDeltaY); moveY(ui->visibility, kDeltaY); moveY(ui->buttonBox, kDeltaY); resizeHeight(this, kDeltaY); } ui->styleLabel->setVisible(mode == Mode::Welcome); ui->style->setVisible(mode == Mode::Welcome); if (mode == Mode::Welcome) { ui->buttonBox->setStandardButtons(QDialogButtonBox::Ok); ui->screenLabel->setVisible(false); ui->screen->setVisible(false); } else { ui->buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); } ui->visibilityLabel->setVisible(mode != Mode::Clone); ui->visibility->setVisible(mode != Mode::Clone); if (mode == Mode::Clone) { constexpr int kDeltaY = -460; moveY(ui->positionLabel, kDeltaY); moveY(ui->position, kDeltaY); moveY(ui->screenLabel, kDeltaY); moveY(ui->screen, kDeltaY); moveY(ui->buttonBox, kDeltaY - 45); resizeHeight(this, kDeltaY - 45); } else if (mode != Mode::Welcome) { ui->styleLabel->setVisible(false); ui->style->setVisible(false); constexpr int kDeltaY = -40; moveY(ui->positionLabel, kDeltaY); moveY(ui->position, kDeltaY); moveY(ui->screenLabel, kDeltaY); moveY(ui->screen, kDeltaY); moveY(ui->visibilityLabel, kDeltaY); moveY(ui->visibility, kDeltaY); moveY(ui->buttonBox, kDeltaY); resizeHeight(this, kDeltaY); } if (isSingleScreen_ || mode == Mode::Welcome) { constexpr int kScreenDeltaY = -40; moveY(ui->visibilityLabel, kScreenDeltaY); moveY(ui->visibility, kScreenDeltaY); moveY(ui->buttonBox, kScreenDeltaY); resizeHeight(this, kScreenDeltaY); } } void AddPanelDialog::accept() { QDialog::accept(); auto position = static_cast(ui->position->currentIndex()); auto screen = ui->screen->currentIndex(); const auto& visibilityText = ui->visibility->currentText(); auto visibility = visibilityText == "Always Visible" ? PanelVisibility::AlwaysVisible : visibilityText == "Intelligent Auto Hide" ? PanelVisibility::IntelligentAutoHide : visibilityText == "Auto Hide" ? PanelVisibility::AutoHide : PanelVisibility::AlwaysOnTop; if (mode_ == Mode::Clone) { model_->cloneDock(dockId_, position, screen); } else { if (mode_ == Mode::Welcome) { const auto& style = ui->style->currentText(); model_->setPanelStyle(style == "Glass 3D" ? PanelStyle::Glass3D_Floating : style == "Glass 2D" ? PanelStyle::Glass2D_Floating : style == "Flat 2D" ? PanelStyle::Flat2D_Floating : PanelStyle::Metal2D_NonFloating); } model_->addDock( position, screen, visibility, ui->showApplicationMenu->isChecked(), ui->showPager->isChecked(), ui->showTaskManager->isChecked(), ui->showTrash->isChecked(), ui->showWifiManager->isChecked(), ui->showVolumeControl->isChecked(), ui->showBatteryIndicator->isChecked(), ui->showKeyboardLayout->isChecked(), ui->showVersionChecker->isChecked(), ui->showClock->isChecked()); model_->maybeAddDockForMultiScreen(); } } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/add_panel_dialog.h000066400000000000000000000027331512233520000230540ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_ADD_PANEL_DIALOG_H_ #define CRYSTALDOCK_ADD_PANEL_DIALOG_H_ #include #include namespace Ui { class AddPanelDialog; } namespace crystaldock { class AddPanelDialog : public QDialog { Q_OBJECT public: enum class Mode { Add, Clone, Welcome }; // Parameter dockId is only needed in Clone mode. AddPanelDialog(QWidget* parent, MultiDockModel* model, int dockId); ~AddPanelDialog(); void setMode(Mode mode); public slots: void accept() override; private: Ui::AddPanelDialog *ui; Mode mode_; MultiDockModel* model_; int dockId_; bool isSingleScreen_; friend class AddPanelDialogTest; }; } // namespace crystaldock #endif // CRYSTALDOCK_ADD_PANEL_DIALOG_H_ dangvd-crystal-dock-7a34e96/src/view/add_panel_dialog.ui000066400000000000000000000212611512233520000232370ustar00rootroot00000000000000 AddPanelDialog 0 0 540 770 Add Panel 70 710 400 32 Qt::Orientation::Horizontal QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok true 60 560 160 26 Panel position 240 550 160 40 1 Top Bottom Left Right 60 605 160 26 Screen 240 595 80 40 60 68 351 23 Optional features: 120 100 300 30 Application Menu true 120 140 300 30 Pager false 60 20 400 23 Please set up your new panel. 120 460 300 30 Clock true 120 180 311 30 Task Manager true 60 515 160 26 Dock style 240 505 160 40 Glass 3D Glass 2D Flat 2D Metal 2D 120 220 300 30 Trash true 120 300 300 30 Volume Control true 120 420 300 30 Version Checker true 120 260 300 30 Wi-Fi Manager true 120 340 300 30 Battery Indicator true 120 380 300 30 Keyboard Layout true 60 650 160 26 Visibility 240 640 240 40 Always Visible Intelligent Auto Hide Auto Hide Always On Top buttonBox accepted() AddPanelDialog accept() 248 254 157 274 buttonBox rejected() AddPanelDialog reject() 316 260 286 274 dangvd-crystal-dock-7a34e96/src/view/appearance_settings_dialog.cc000066400000000000000000000171101512233520000253150ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "appearance_settings_dialog.h" #include "ui_appearance_settings_dialog.h" #include namespace crystaldock { AppearanceSettingsDialog::AppearanceSettingsDialog(QWidget* parent, MultiDockModel* model) : QDialog(parent), ui(new Ui::AppearanceSettingsDialog), model_(model) { ui->setupUi(this); setWindowFlag(Qt::Tool); backgroundColor_ = new ColorButton(this); backgroundColor_->setGeometry(QRect(260, 210, 80, 40)); borderColor_ = new ColorButton(this); borderColor_->setGeometry(QRect(700, 210, 80, 40)); activeIndicatorColor_ = new ColorButton(this); activeIndicatorColor_->setGeometry(QRect(260, 270, 80, 40)); inactiveIndicatorColor_ = new ColorButton(this); inactiveIndicatorColor_->setGeometry(QRect(700, 270, 80, 40)); connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonClicked(QAbstractButton*))); connect(ui->enableZooming, &QCheckBox::checkStateChanged, this, &AppearanceSettingsDialog::onEnableZoomingChanged); loadData(); } AppearanceSettingsDialog::~AppearanceSettingsDialog() { delete ui; } void AppearanceSettingsDialog::accept() { QDialog::accept(); saveData(); } void AppearanceSettingsDialog::buttonClicked(QAbstractButton* button) { auto role = ui->buttonBox->buttonRole(button); if (role == QDialogButtonBox::ApplyRole) { saveData(); } else if (role == QDialogButtonBox::ResetRole) { resetData(); } } void AppearanceSettingsDialog::onEnableZoomingChanged() { const auto enableZooming = ui->enableZooming->isChecked(); ui->maxSize->setEnabled(enableZooming); if (enableZooming) { ui->maxSize->setValue(prevMaxIconSize_); } else { prevMaxIconSize_ = ui->maxSize->value(); ui->maxSize->setValue(ui->minSize->value()); } } void AppearanceSettingsDialog::loadData() { const auto enableZooming = model_->minIconSize() < model_->maxIconSize(); ui->enableZooming->setChecked(enableZooming); ui->zoomingAnimationSpeed->setValue(model_->zoomingAnimationSpeed()); ui->minSize->setValue(model_->minIconSize()); ui->maxSize->setValue(model_->maxIconSize()); ui->maxSize->setEnabled(enableZooming); prevMaxIconSize_ = model_->maxIconSize(); ui->spacingFactor->setValue(model_->spacingFactor()); QColor backgroundColor = model_->isGlass() ? model_->backgroundColor() : model_->isFlat2D() ? model_->backgroundColor2D() : model_->backgroundColorMetal2D(); backgroundColor_->setColor(QColor(backgroundColor.rgb())); ui->backgroundTransparency->setValue(alphaFToTransparencyPercent(backgroundColor.alphaF())); borderColor_->setColor(model_->isGlass() ? model_->borderColor() : model_->borderColorMetal2D()); borderColor_->setVisible(!model_->isFlat2D()); ui->borderColorLabel->setVisible(!model_->isFlat2D()); activeIndicatorColor_->setColor( model_->isGlass() ? model_->activeIndicatorColor() : model_->isFlat2D() ? model_->activeIndicatorColor2D() : model_->activeIndicatorColorMetal2D()); inactiveIndicatorColor_->setColor( model_->isGlass() ? model_->inactiveIndicatorColor() : model_->isFlat2D() ? model_->inactiveIndicatorColor2D() : model_->inactiveIndicatorColorMetal2D()); ui->showTooltip->setChecked(model_->showTooltip()); ui->tooltipFontSize->setValue(model_->tooltipFontSize()); ui->floatingMargin->setValue(model_->floatingMargin()); ui->floatingMargin->setEnabled(model_->isFloating()); ui->bouncingLauncherIcon->setChecked(model_->bouncingLauncherIcon()); } void AppearanceSettingsDialog::resetData() { const auto enableZooming = kDefaultMinSize < kDefaultMaxSize; ui->enableZooming->setChecked(enableZooming); ui->zoomingAnimationSpeed->setValue(kDefaultZoomingAnimationSpeed); ui->minSize->setValue(kDefaultMinSize); ui->maxSize->setValue(kDefaultMaxSize); ui->maxSize->setEnabled(enableZooming); ui->spacingFactor->setValue(kDefaultSpacingFactor); backgroundColor_->setColor(QColor( model_->isGlass() ? kDefaultBackgroundColor : model_->isFlat2D() ? kDefaultBackgroundColor2D : kDefaultBackgroundColorMetal2D)); ui->backgroundTransparency->setValue(alphaFToTransparencyPercent( model_->isMetal2D() ? kDefaultBackgroundAlphaMetal2D : kDefaultBackgroundAlpha)); borderColor_->setColor(QColor(model_->isGlass() ? kDefaultBorderColor : kDefaultBorderColorMetal2D)); activeIndicatorColor_->setColor(QColor( model_->isGlass() ? kDefaultActiveIndicatorColor : model_->isFlat2D() ? kDefaultActiveIndicatorColor2D : kDefaultActiveIndicatorColorMetal2D)); inactiveIndicatorColor_->setColor(QColor( model_->isGlass() ? kDefaultInactiveIndicatorColor : model_->isFlat2D() ? kDefaultInactiveIndicatorColor2D : kDefaultInactiveIndicatorColorMetal2D)); ui->showTooltip->setChecked(kDefaultShowTooltip); ui->tooltipFontSize->setValue(kDefaultTooltipFontSize); ui->floatingMargin->setValue(kDefaultFloatingMargin); ui->bouncingLauncherIcon->setChecked(kDefaultBouncingLauncherIcon); } void AppearanceSettingsDialog::saveData() { model_->setMinIconSize(ui->minSize->value()); model_->setMaxIconSize(ui->maxSize->value()); model_->setZoomingAnimationSpeed(ui->zoomingAnimationSpeed->value()); model_->setSpacingFactor(ui->spacingFactor->value()); QColor backgroundColor(backgroundColor_->color()); backgroundColor.setAlphaF(transparencyPercentToAlphaF(ui->backgroundTransparency->value())); if (model_->isGlass()) { model_->setBackgroundColor(backgroundColor); } else if (model_->isFlat2D()) { model_->setBackgroundColor2D(backgroundColor); } else { model_->setBackgroundColorMetal2D(backgroundColor); } model_->setBorderColor(borderColor_->color()); if (model_->isGlass()) { model_->setActiveIndicatorColor(activeIndicatorColor_->color()); } else if (model_->isFlat2D()) { model_->setActiveIndicatorColor2D(activeIndicatorColor_->color()); } else { model_->setActiveIndicatorColorMetal2D(activeIndicatorColor_->color()); } if (model_->isGlass()) { model_->setInactiveIndicatorColor(inactiveIndicatorColor_->color()); } else if (model_->isFlat2D()) { model_->setInactiveIndicatorColor2D(inactiveIndicatorColor_->color()); } else { model_->setInactiveIndicatorColorMetal2D(inactiveIndicatorColor_->color()); } model_->setShowTooltip(ui->showTooltip->isChecked()); model_->setTooltipFontSize(ui->tooltipFontSize->value()); model_->setFloatingMargin(ui->floatingMargin->value()); model_->setBouncingLauncherIcon(ui->bouncingLauncherIcon->isChecked()); model_->saveAppearanceConfig(); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/appearance_settings_dialog.h000066400000000000000000000033461512233520000251650ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_APPEARANCE_SETTINGS_DIALOG_H_ #define CRYSTALDOCK_APPEARANCE_SETTINGS_DIALOG_H_ #include #include "color_button.h" #include namespace Ui { class AppearanceSettingsDialog; } namespace crystaldock { class AppearanceSettingsDialog : public QDialog { Q_OBJECT public: AppearanceSettingsDialog(QWidget* parent, MultiDockModel* model); ~AppearanceSettingsDialog(); void reload() { loadData(); } public slots: void accept() override; void buttonClicked(QAbstractButton* button); void onEnableZoomingChanged(); private: void loadData(); void resetData(); void saveData(); Ui::AppearanceSettingsDialog *ui; ColorButton* backgroundColor_; ColorButton* borderColor_; ColorButton* activeIndicatorColor_; ColorButton* inactiveIndicatorColor_; MultiDockModel* model_; int prevMaxIconSize_ = 0; friend class AppearanceSettingsDialogTest; }; } // namespace crystaldock #endif // CRYSTALDOCK_APPEARANCE_SETTINGS_DIALOG_H_ dangvd-crystal-dock-7a34e96/src/view/appearance_settings_dialog.ui000066400000000000000000000221061512233520000253460ustar00rootroot00000000000000 AppearanceSettingsDialog 0 0 820 540 Appearance Settings 34 470 730 36 Qt::Orientation::Horizontal QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::RestoreDefaults true 40 90 220 40 Minimum icon size 260 90 81 40 16 64 8 440 90 220 40 Maximum icon size 700 90 81 40 32 192 8 440 150 220 40 Panel transparency 40 210 220 40 Background color 440 210 220 40 Border color 440 330 220 40 Tooltip font size 700 330 81 40 8 28 2 40 150 220 40 Icon spacing factor 700 150 81 40 % 100 5 100 260 150 81 40 1 0.100000000000000 0.900000000000000 0.100000000000000 0.500000000000000 440 390 200 40 Floating margin 700 390 81 40 2 32 2 6 40 270 220 40 Indicator color (active) 440 270 220 40 Indicator color (inactive) 40 390 300 40 Bouncing launcher icon 40 330 300 40 Show tooltip 440 30 250 40 Zooming animation speed 700 30 81 40 1 30 1 16 40 30 300 40 Enable zooming effect false buttonBox accepted() AppearanceSettingsDialog accept() 248 254 157 274 buttonBox rejected() AppearanceSettingsDialog reject() 316 260 286 274 dangvd-crystal-dock-7a34e96/src/view/application_menu.cc000066400000000000000000000243561512233520000233200ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "application_menu.h" #include #include #include #include #include #include #include #include #include #include "display/window_system.h" #include "dock_panel.h" #include "program.h" #include #include namespace crystaldock { int ApplicationMenuStyle::pixelMetric( PixelMetric metric, const QStyleOption *option, const QWidget *widget) const { if (metric == QStyle::PM_SmallIconSize) { return model_->applicationMenuIconSize(); } return QProxyStyle::pixelMetric(metric, option, widget); } ApplicationMenu::ApplicationMenu( DockPanel *parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize) : IconBasedDockItem(parent, model, "" /* label */, orientation, model->applicationMenuIcon(), minSize, maxSize), showingMenu_(false), style_(model) { menu_.setAttribute(Qt::WA_TranslucentBackground); menu_.setStyle(&style_); menu_.setStyleSheet(getStyleSheet()); loadConfig(); buildMenu(); createContextMenu(); connect(&menu_, &QMenu::aboutToHide, this, [this]() { showingMenu_ = false; parent_->setShowingPopup(false); parent_->update(); }); connect(&contextMenu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); connect(model_, SIGNAL(applicationMenuConfigChanged()), this, SLOT(reloadMenu())); } void ApplicationMenu::draw(QPainter* painter) const { if (showingMenu_) { const auto x = left_ + getWidth() / 2; const auto y = top_ + getHeight() / 2; if (parent_->isGlass()) { const QColor baseColor = model_->activeIndicatorColor(); // Size (width if horizontal, or height if vertical) of the indicator. const auto size = DockPanel::kIndicatorSizeGlass; drawIndicator(orientation_, x, parent_->taskIndicatorPos(), parent_->taskIndicatorPos(), y, size, DockPanel::k3DPanelThickness, baseColor, painter); } else if (parent_->isFlat2D()) { const auto baseColor = model_->activeIndicatorColor2D(); const auto size = DockPanel::kIndicatorSizeFlat2D; drawIndicatorFlat2D(orientation_, x, parent_->taskIndicatorPos(), parent_->taskIndicatorPos(), y, size, baseColor, painter); } else { // Metal 2D. const auto baseColor = model_->activeIndicatorColorMetal2D(); const auto size = DockPanel::kIndicatorSizeMetal2D; drawIndicatorMetal2D(parent_->position(), x, parent_->taskIndicatorPos(), parent_->taskIndicatorPos(), y, size, baseColor, painter); } } IconBasedDockItem::draw(painter); } void ApplicationMenu::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { parent_->setShowingPopup(true); // Acknowledge. showingMenu_ = true; parent_->update(); resetSearchMenu(); const int x = parent_->isBottom() && parent_->is3D() ? left_ : left_ - parent_->itemSpacing(); menu_.exec(parent_->mapToGlobal(QPoint(x, top_))); } else if (e->button() == Qt::RightButton) { showPopupMenu(&contextMenu_); } } void ApplicationMenu::reloadMenu() { menu_.clear(); searchMenu_ = nullptr; buildMenu(); } void ApplicationMenu::searchApps(const QString& searchText_) { if (searchMenu_ == nullptr) { return; } QString text = searchText_.trimmed(); if (text.isEmpty()) { resetSearchMenu(); return; } const auto actions = searchMenu_->actions(); for (int i = 1; i < actions.size(); ++i) { searchMenu_->removeAction(actions[i]); } // We need to limit max number of results to avoid the sub-menu being pushed up too much. for (const auto& entry : model_->searchApplications(text, maxNumResults_)) { addEntry(entry, searchMenu_); } if (parent_->isBottom()) { // Work-around for sub-menu alignment issue on Wayland. patchMenu(maxNumResults_ + 1, model_->applicationMenuIconSize(), searchMenu_); } } bool ApplicationMenu::eventFilter(QObject* object, QEvent* event) { if (event->type() == QEvent::MouseButtonPress) { auto* activeItem = (dynamic_cast(object))->activeAction(); QMouseEvent* mouseEvent = dynamic_cast(event); if (mouseEvent && mouseEvent->button() == Qt::LeftButton && activeItem) { startMousePos_ = mouseEvent->pos(); draggedEntry_ = activeItem->data().toString(); } } else if (event->type() == QEvent::MouseMove) { QMouseEvent* mouseEvent = dynamic_cast(event); if (mouseEvent && mouseEvent->buttons() & Qt::LeftButton) { const int distance = (mouseEvent->pos() - startMousePos_).manhattanLength(); if (distance >= QApplication::startDragDistance() && !draggedEntry_.isEmpty()) { // Start drag. QMimeData* mimeData = new QMimeData; mimeData->setData("text/uri-list", QUrl::fromLocalFile(draggedEntry_).toEncoded()); QDrag* drag = new QDrag(this); drag->setMimeData(mimeData); drag->exec(Qt::CopyAction); } } } return QObject::eventFilter(object, event); } QString ApplicationMenu::getStyleSheet() { QColor bgColor = model_->backgroundColor(); bgColor.setAlphaF(model_->applicationMenuBackgroundAlpha()); bgColor = bgColor.darker(); QColor borderColor = model_->borderColor(); return " \ QMenu { \ background-color: " % bgColor.name(QColor::HexArgb) % ";" " margin: 1px; \ padding: 2px; \ border: 1px transparent; \ border-radius: 3px; \ } \ \ QMenu::item { \ font: bold; \ color: white; \ background-color: transparent; \ padding: 4px 45px 4px 45px; \ } \ \ QMenu::item:selected { \ background-color: " % bgColor.name(QColor::HexArgb) % ";" " border: 1px solid " % borderColor.name() % ";" " border-radius: 3px; \ } \ \ QMenu::separator { \ margin: 5px; \ height: 1px; \ background: " % borderColor.name() % ";" "}"; } void ApplicationMenu::loadConfig() { setLabel(model_->applicationMenuName()); font_ = menu_.font(); font_.setPointSize(model_->applicationMenuFontSize()); font_.setBold(true); menu_.setFont(font_); } void ApplicationMenu::buildMenu() { addSearchMenu(); menu_.addSeparator(); addToMenu(model_->applicationMenuCategories()); menu_.addSeparator(); addToMenu(model_->applicationMenuSystemCategories()); const auto numSubMenus = menu_.actions().size(); for (int i = 0; i < numSubMenus; ++i) { QMenu* menu = menu_.actions()[i]->menu(); if (menu != nullptr && parent_->isBottom()) { // Work-around for sub-menu alignment issue on Wayland. patchMenu(numSubMenus - i, model_->applicationMenuIconSize(), menu); } } maxNumResults_ = menu_.actions().size() - 2; } void ApplicationMenu::addSearchMenu() { searchMenu_ = menu_.addMenu(loadIcon("edit-find"), "Search"); searchMenu_->setAttribute(Qt::WA_TranslucentBackground); searchMenu_->setStyle(&style_); searchMenu_->setFont(font_); searchMenu_->installEventFilter(this); searchText_ = new QLineEdit(searchMenu_); searchText_->setMinimumWidth(250); searchText_->setPlaceholderText("Type here to search"); // A work-around as using QWidgetAction somehow causes a memory issue // when quitting the dock. searchMenu_->addAction(loadIcon("edit-find"), " "); connect(searchText_, SIGNAL(textEdited(const QString&)), this, SLOT(searchApps(const QString&))); } void ApplicationMenu::addToMenu(const std::vector& categories) { for (const auto& category : categories) { if (category.name == ApplicationMenuConfig::kUncategorized || category.entries.empty()) { continue; } QMenu* menu = menu_.addMenu(loadIcon(category.icon), category.displayName); menu->setAttribute(Qt::WA_TranslucentBackground); menu->setStyle(&style_); menu->setFont(font_); menu->installEventFilter(this); for (const auto& entry : category.entries) { addEntry(entry, menu); } } } void ApplicationMenu::addEntry(const ApplicationEntry &entry, QMenu *menu) { if (entry.hidden) { return; } QAction* action = menu->addAction(loadIcon(entry.icon), entry.name, this, [entry]() { Program::launch(entry.command); }); action->setData(entry.desktopFile); } void ApplicationMenu::resetSearchMenu() { searchText_->clear(); searchText_->setFocus(); const auto actions = searchMenu_->actions(); for (int i = 1; i < actions.size(); ++i) { searchMenu_->removeAction(actions[i]); } if (parent_->isBottom()) { // Work-around for sub-menu alignment issue on Wayland. patchMenu(maxNumResults_ + 1, model_->applicationMenuIconSize(), searchMenu_); } } QIcon ApplicationMenu::loadIcon(const QString &icon) { return QIcon::fromTheme(icon); } void ApplicationMenu::createContextMenu() { contextMenu_.addSection("Application Menu"); contextMenu_.addAction(QIcon::fromTheme("configure"), QString("Application Menu &Settings"), parent_, [this] { parent_->minimize(); QTimer::singleShot(DockPanel::kExecutionDelayMs, [this]{ parent_->showApplicationMenuSettingsDialog(); }); }); contextMenu_.addSeparator(); parent_->addPanelSettings(&contextMenu_); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/application_menu.h000066400000000000000000000067171512233520000231630ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_APPLICATION_MENU_H_ #define CRYSTALDOCK_APPLICATION_MENU_H_ #include "icon_based_dock_item.h" #include #include #include #include #include #include #include #include #include #include namespace crystaldock { class ApplicationMenuStyle : public QProxyStyle { public: ApplicationMenuStyle(const MultiDockModel* model) : model_(model) {} int pixelMetric(PixelMetric metric, const QStyleOption *option = Q_NULLPTR, const QWidget *widget = Q_NULLPTR) const override; private: const MultiDockModel* model_; }; // The application menu item on the dock. // // Left-clicking the item shows a cascading popup menu that contains entries // for all applications organized by categories. The menu uses a custom style // e.g. bigger icon size and the same translucent effect as the dock's. // // Supports drag-and-drop as a drag source. // What it means is that you can drag an application entry from the menu // to other widgets/applications. It doesn't support drag-and-drop within the // menu itself. class ApplicationMenu : public QObject, public IconBasedDockItem { Q_OBJECT public: ApplicationMenu( DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize); virtual ~ApplicationMenu() = default; void draw(QPainter* painter) const override; void mousePressEvent(QMouseEvent* e) override; void loadConfig() override; QSize getMenuSize() { return menu_.sizeHint(); } public slots: void reloadMenu(); void searchApps(const QString& searchText); protected: // To support drag applications from the sub-menu to Edit Launchers dialog. bool eventFilter(QObject* object, QEvent* event) override; private: QString getStyleSheet(); QIcon loadIcon(const QString& icon); // Builds the menu from the application entries; void buildMenu(); void addSearchMenu(); void addToMenu(const std::vector& categories); void addEntry(const ApplicationEntry& entry, QMenu* menu); void resetSearchMenu(); void createContextMenu(); // The cascading popup menu that contains all application entries. QMenu menu_; bool showingMenu_; ApplicationMenuStyle style_; QFont font_; // Drag support. // Starting mouse position, used for minimum drag distance check. QPoint startMousePos_; // The desktop file associated with the application entry being dragged. QString draggedEntry_; QMenu* searchMenu_; QLineEdit* searchText_; unsigned int maxNumResults_; // Context (right-click) menu. QMenu contextMenu_; }; } // namespace crystaldock #endif // CRYSTALDOCK_APPLICATION_MENU_H_ dangvd-crystal-dock-7a34e96/src/view/application_menu_settings_dialog.cc000066400000000000000000000053511512233520000265510ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "application_menu_settings_dialog.h" #include "ui_application_menu_settings_dialog.h" #include namespace crystaldock { ApplicationMenuSettingsDialog::ApplicationMenuSettingsDialog(QWidget* parent, MultiDockModel* model) : QDialog(parent), ui(new Ui::ApplicationMenuSettingsDialog), model_(model) { ui->setupUi(this); setWindowFlag(Qt::Tool); connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonClicked(QAbstractButton*))); loadData(); } ApplicationMenuSettingsDialog::~ApplicationMenuSettingsDialog() { delete ui; } void ApplicationMenuSettingsDialog::accept() { QDialog::accept(); saveData(); } void ApplicationMenuSettingsDialog::buttonClicked(QAbstractButton *button) { auto role = ui->buttonBox->buttonRole(button); if (role == QDialogButtonBox::ApplyRole) { saveData(); } else if (role == QDialogButtonBox::ResetRole) { resetData(); } } void ApplicationMenuSettingsDialog::loadData() { ui->name->setText(model_->applicationMenuName()); ui->iconSize->setValue(model_->applicationMenuIconSize()); ui->fontSize->setValue(model_->applicationMenuFontSize()); ui->backgroundTransparency->setValue( alphaFToTransparencyPercent(model_->applicationMenuBackgroundAlpha())); } void ApplicationMenuSettingsDialog::resetData() { ui->name->setText(kDefaultApplicationMenuName); ui->iconSize->setValue(kDefaultApplicationMenuIconSize); ui->fontSize->setValue(kDefaultApplicationMenuFontSize); ui->backgroundTransparency->setValue( alphaFToTransparencyPercent(kDefaultApplicationMenuBackgroundAlpha)); } void ApplicationMenuSettingsDialog::saveData() { model_->setApplicationMenuName(ui->name->text()); model_->setApplicationMenuIconSize(ui->iconSize->value()); model_->setApplicationMenuFontSize(ui->fontSize->value()); model_->setApplicationMenuBackgroundAlpha( transparencyPercentToAlphaF(ui->backgroundTransparency->value())); model_->saveAppearanceConfig(); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/application_menu_settings_dialog.h000066400000000000000000000031151512233520000264070ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_APPLICATION_MENU_SETTINGS_DIALOG_H_ #define CRYSTALDOCK_APPLICATION_MENU_SETTINGS_DIALOG_H_ #include #include #include namespace Ui { class ApplicationMenuSettingsDialog; } namespace crystaldock { class ApplicationMenuSettingsDialog : public QDialog { Q_OBJECT public: ApplicationMenuSettingsDialog(QWidget* parent, MultiDockModel* model); ~ApplicationMenuSettingsDialog(); void reload() { loadData(); } public slots: void accept() override; void buttonClicked(QAbstractButton* button); private: void loadData(); void saveData(); void resetData(); Ui::ApplicationMenuSettingsDialog *ui; MultiDockModel* model_; friend class ApplicationMenuSettingsDialogTest; }; } // namespace crystaldock #endif // CRYSTALDOCK_APPLICATION_MENU_SETTINGS_DIALOG_H_ dangvd-crystal-dock-7a34e96/src/view/application_menu_settings_dialog.ui000066400000000000000000000102461512233520000266000ustar00rootroot00000000000000 ApplicationMenuSettingsDialog 0 0 500 370 Application Menu Settings 0 300 500 32 Qt::Horizontal QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults true 70 40 91 30 Name 190 30 241 36 70 160 94 30 Font size 70 220 281 30 Background transparency 190 150 63 44 10 40 2 352 210 81 44 % 100 5 70 100 94 30 Icon size 190 90 63 44 32 64 8 buttonBox accepted() ApplicationMenuSettingsDialog accept() 248 254 157 274 buttonBox rejected() ApplicationMenuSettingsDialog reject() 316 260 286 274 dangvd-crystal-dock-7a34e96/src/view/battery_indicator.cc000066400000000000000000000115631512233520000234730ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "battery_indicator.h" #include #include "dock_panel.h" #include namespace crystaldock { BatteryIndicator::BatteryIndicator(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize) : IconBasedDockItem(parent, model, kLabel, orientation, kIcon, minSize, maxSize), updateTimer_(new QTimer(this)), process_(nullptr) { createMenu(); // Sets up update timer. QTimer::singleShot(500, this, [this]() { connect(updateTimer_, &QTimer::timeout, this, &BatteryIndicator::refreshBatteryInfo); updateTimer_->start(kUpdateInterval); batteryDevice_ = getBatteryDevice(); }); connect(&contextMenu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); } BatteryIndicator::~BatteryIndicator() { if (process_ && process_->state() != QProcess::NotRunning) { process_->kill(); process_->waitForFinished(1000); } } void BatteryIndicator::mousePressEvent(QMouseEvent* e) { if (e->button() == Qt::LeftButton) { if (commandExists({kCommand}).isEmpty()) { QMessageBox::warning(parent_, "Command not found", QString("Command '") + kCommand + "' not found. This is required by the " + kLabel + " component."); return; } } else if (e->button() == Qt::RightButton) { showPopupMenu(&contextMenu_); } } QString BatteryIndicator::getLabel() const { return batteryDevice_.isNull() ? kLabel : batteryDevice_.isEmpty() ? "Battery: Not found" : "Battery: " + QString::number(batteryLevel_) + "%" + (isCharging_ ? " (charging)" : ""); } void BatteryIndicator::refreshBatteryInfo() { // Prevent concurrent processes if (process_ && process_->state() != QProcess::NotRunning) { return; } if (batteryDevice_.isEmpty()) { return; } process_ = new QProcess(parent_); connect(process_, QOverload::of(&QProcess::finished), [this](int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode == 0) { bool isCharging = false; int batteryLevel = 0; QRegularExpression chargingRe(R"(state:(\s+)charging)"); QRegularExpression percentageRe(R"(percentage:(\s+)(\d+.?\d*)(\s*)%)"); for (const QByteArray& line : process_->readAllStandardOutput().split('\n')) { if (chargingRe.match(line).hasMatch()) { isCharging = true; } auto match = percentageRe.match(line); if (match.hasMatch()) { batteryLevel = static_cast(std::round(match.captured(2).toFloat())); } } if (isCharging_ != isCharging || batteryLevel_ != batteryLevel) { isCharging_ = isCharging; batteryLevel_ = batteryLevel; updateUi(); } } process_->deleteLater(); process_ = nullptr; }); process_->start(kCommand, QStringList() << "-i" << batteryDevice_); } QString BatteryIndicator::getBatteryDevice() { QProcess process; process.start(kCommand, QStringList() << "--enumerate"); process.waitForFinished(1000 /*msecs*/); for (const QByteArray& device : process.readAllStandardOutput().split('\n')) { if (device.toLower().contains("battery")) { return device; } } // Returns an empty string instead of null string to specify that // the battery device was not found. return QString(""); } void BatteryIndicator::createMenu() { contextMenu_.addSection(kLabel); contextMenu_.addSeparator(); parent_->addPanelSettings(&contextMenu_); } void BatteryIndicator::updateUi() { QString iconName = kIcon; if (!batteryDevice_.isEmpty() && batteryLevel_ > 0 && !isCharging_) { if (batteryLevel_ < 20) { iconName = "battery-low"; } else if (batteryLevel_ < 40) { iconName = "battery-caution"; } } setIconName(iconName); parent_->update(); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/battery_indicator.h000066400000000000000000000041121512233520000233250ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTAL_DOCK_BATTERY_INDICATOR_H_ #define CRYSTAL_DOCK_BATTERY_INDICATOR_H_ #include "icon_based_dock_item.h" #include #include #include namespace crystaldock { // A battery indicator that integrates with upower. class BatteryIndicator : public QObject, public IconBasedDockItem { Q_OBJECT public: BatteryIndicator(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize); virtual ~BatteryIndicator(); void mousePressEvent(QMouseEvent* e) override; QString getLabel() const override; bool beforeTask(const QString& program) override { return false; } static QString getBatteryDevice(); public slots: void refreshBatteryInfo(); private: static constexpr char kCommand[] = "upower"; static constexpr char kLabel[] = "Battery Indicator"; static constexpr char kIcon[] = "battery"; static constexpr int kUpdateInterval = 1000; // 1 second. // Creates the context menu. void createMenu(); void updateUi(); QString batteryDevice_; int batteryLevel_ = 0; // in percentage. bool isCharging_ = false; // Update timer. QTimer* updateTimer_; // upower process. QProcess* process_ = nullptr; // Right-click context menu. QMenu contextMenu_; }; } // namespace crystaldock #endif // CRYSTAL_DOCK_BATTERY_INDICATOR_H_ dangvd-crystal-dock-7a34e96/src/view/calendar.cc000066400000000000000000000022761512233520000215370ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "calendar.h" #include namespace crystaldock { Calendar::Calendar(QWidget* parent) : QDialog(parent), calendar_(this) { setWindowFlag(Qt::Tool); setWindowTitle("Calendar"); calendar_.setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader); resize(calendar_.sizeHint()); } void Calendar::showCalendar() { calendar_.setSelectedDate(QDate::currentDate()); show(); raise(); activateWindow(); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/calendar.h000066400000000000000000000023531512233520000213750ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_CALENDAR_H_ #define CRYSTALDOCK_CALENDAR_H_ #include #include namespace crystaldock { // A calendar widget. This is shown when the user clicks on the clock. class Calendar : public QDialog { public: Calendar(QWidget* parent); // Toggles showing the calendar. // // This also resets the selected date to the current date. void showCalendar(); private: QCalendarWidget calendar_; }; } // namespace crystaldock #endif // CRYSTALDOCK_CALENDAR_H_ dangvd-crystal-dock-7a34e96/src/view/clock.cc000066400000000000000000000131241512233520000210530ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "clock.h" #include #include #include #include #include #include #include "dock_panel.h" #include #include namespace crystaldock { constexpr float Clock::kWhRatio; constexpr float Clock::kDelta; Clock::Clock(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize) : IconlessDockItem(parent, model, "" /* label */, orientation, minSize, maxSize, kWhRatio), calendar_(parent), fontFamilyGroup_(this) { createMenu(); loadConfig(); QTimer* timer = new QTimer(this); connect(timer, SIGNAL(timeout()), this, SLOT(updateTime())); timer->start(1000); // update the time every second. connect(&menu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); } void Clock::draw(QPainter *painter) const { const QString timeFormat = model_->use24HourClock() ? "hh:mm" : "hh:mm AP"; const QString time = QTime::currentTime().toString(timeFormat); // The reference time used to calculate the font size. const QString referenceTime = QTime(8, 8).toString(timeFormat); const int margin = parent_->isHorizontal() ? getHeight() * 0.1 : 0; const auto x = left_ + margin; const auto y = top_; const auto w = getWidth() - margin; const auto h = getHeight(); painter->setFont(adjustFontSize(w, h, referenceTime, model_->clockFontScaleFactor(), model_->clockFontFamily())); painter->setRenderHint(QPainter::TextAntialiasing); if (size_ > minSize_) { drawBorderedText(x, y , w, h, Qt::AlignCenter, time, 2 /* borderWidth */, Qt::black, Qt::white, painter, /*simplified=*/ true); } else { painter->setPen(Qt::white); painter->drawText(x, y, w, h, Qt::AlignCenter, time); } } void Clock::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { calendar_.showCalendar(); } else if (e->button() == Qt::RightButton) { // In case other docks have changed the config. loadConfig(); showPopupMenu(&menu_); } } QString Clock::getLabel() const { return QLocale::system().toString(QDate::currentDate(), QLocale::LongFormat); } void Clock::updateTime() { parent_->update(); } void Clock::setFontScaleFactor(float fontScaleFactor) { largeFontAction_->setChecked( fontScaleFactor > kLargeClockFontScaleFactor - kDelta); mediumFontAction_->setChecked( fontScaleFactor > kMediumClockFontScaleFactor - kDelta && fontScaleFactor < kMediumClockFontScaleFactor + kDelta); smallFontAction_->setChecked( fontScaleFactor < kSmallClockFontScaleFactor + kDelta); } void Clock::setLargeFont() { setFontScaleFactor(kLargeClockFontScaleFactor); saveConfig(); } void Clock::setMediumFont() { setFontScaleFactor(kMediumClockFontScaleFactor); saveConfig(); } void Clock::setSmallFont() { setFontScaleFactor(kSmallClockFontScaleFactor); saveConfig(); } void Clock::createMenu() { menu_.addSection("Clock"); use24HourClockAction_ = menu_.addAction( QString("Use 24-hour Clock"), this, [this] { saveConfig(); }); use24HourClockAction_->setCheckable(true); QMenu* fontFamily = menu_.addMenu(QString("Font Family")); for (const auto& family : getBaseFontFamilies()) { auto fontFamilyAction = fontFamily->addAction(family, this, [this, family]{ model_->setClockFontFamily(family); model_->saveAppearanceConfig(true /* repaintOnly */); }); fontFamilyAction->setCheckable(true); fontFamilyAction->setActionGroup(&fontFamilyGroup_); fontFamilyAction->setChecked(family == model_->clockFontFamily()); } QMenu* fontSize = menu_.addMenu(QString("Font Size")); largeFontAction_ = fontSize->addAction(QString("Large Font"), this, SLOT(setLargeFont())); largeFontAction_->setCheckable(true); mediumFontAction_ = fontSize->addAction(QString("Medium Font"), this, SLOT(setMediumFont())); mediumFontAction_->setCheckable(true); smallFontAction_ = fontSize->addAction(QString("Small Font"), this, SLOT(setSmallFont())); smallFontAction_->setCheckable(true); menu_.addSeparator(); parent_->addPanelSettings(&menu_); } void Clock::loadConfig() { use24HourClockAction_->setChecked(model_->use24HourClock()); setFontScaleFactor(model_->clockFontScaleFactor()); } void Clock::saveConfig() { model_->setUse24HourClock(use24HourClockAction_->isChecked()); model_->setClockFontScaleFactor(fontScaleFactor()); model_->saveAppearanceConfig(true /* repaintOnly */); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/clock.h000066400000000000000000000044151512233520000207200ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_CLOCK_H_ #define CRYSTALDOCK_CLOCK_H_ #include "iconless_dock_item.h" #include #include #include #include #include #include "calendar.h" namespace crystaldock { // A digital clock. class Clock : public QObject, public IconlessDockItem { Q_OBJECT public: Clock(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize); virtual ~Clock() = default; void draw(QPainter* painter) const override; void mousePressEvent(QMouseEvent* e) override; void loadConfig() override; QString getLabel() const override; bool beforeTask(const QString& program) override { return false; } public slots: void updateTime(); void setFontScaleFactor(float fontScaleFactor); void setLargeFont(); void setMediumFont(); void setSmallFont(); private: static constexpr float kWhRatio = 2.9; static constexpr float kDelta = 0.01; float fontScaleFactor() { return largeFontAction_->isChecked() ? kLargeClockFontScaleFactor : mediumFontAction_->isChecked() ? kMediumClockFontScaleFactor : kSmallClockFontScaleFactor; } // Creates the context menu. void createMenu(); void saveConfig(); Calendar calendar_; // Context menu. QMenu menu_; QAction* use24HourClockAction_; QAction* largeFontAction_; QAction* mediumFontAction_; QAction* smallFontAction_; QActionGroup fontFamilyGroup_; }; } // namespace crystaldock #endif // CRYSTALDOCK_CLOCK_H_ dangvd-crystal-dock-7a34e96/src/view/color_button.cc000066400000000000000000000025131512233520000224710ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "color_button.h" #include #include namespace crystaldock { ColorButton::ColorButton(QWidget *parent) : QPushButton(parent) { setAutoFillBackground(true); connect(this, SIGNAL(clicked()), this, SLOT(showColorDialog())); } void ColorButton::setColor(const QColor& color) { color_ = color; QPalette pal = palette(); pal.setColor(QPalette::Button, color); setPalette(pal); update(); } void ColorButton::showColorDialog() { QColor newColor = QColorDialog::getColor(color_); if (newColor.isValid()) { setColor(newColor); } } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/color_button.h000066400000000000000000000022651512233520000223370ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_COLOR_BUTTON_H_ #define CRYSTALDOCK_COLOR_BUTTON_H_ #include #include namespace crystaldock { class ColorButton : public QPushButton { Q_OBJECT public: ColorButton(QWidget *parent = nullptr); QColor color() { return color_; } void setColor(const QColor& icon); public slots: void showColorDialog(); private: QColor color_; }; } // namespace crystaldock #endif // CRYSTALDOCK_COLOR_BUTTON_H_ dangvd-crystal-dock-7a34e96/src/view/desktop_selector.cc000066400000000000000000000122041512233520000233270ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "desktop_selector.h" #include #include #include #include #include #include #include #include #include "display/window_system.h" #include "dock_panel.h" #include #include namespace crystaldock { DesktopSelector::DesktopSelector(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize, const VirtualDesktopInfo& desktop, int screen) : IconBasedDockItem(parent, model, QString::fromStdString(desktop.name), orientation, "" /* no icon yet */, minSize, maxSize), desktopEnv_(DesktopEnv::getDesktopEnv()), desktop_(desktop), screen_(screen), desktopWidth_(parent->screenGeometry().width()), desktopHeight_(parent->screenGeometry().height()), hasCustomWallpaper_(false) { createMenu(); loadConfig(); connect(WindowSystem::self(), SIGNAL(desktopNameChanged(std::string_view, std::string_view)), this, SLOT(onDesktopNameChanged(std::string_view, std::string_view))); connect(&menu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); } void DesktopSelector::draw(QPainter* painter) const { if (hasCustomWallpaper_) { IconBasedDockItem::draw(painter); } else { // Draw rectangles with desktop numbers if no custom wallpapers set. QColor fillColor = model_->backgroundColor().lighter(); fillColor.setAlphaF(0.42); painter->fillRect(left_, top_, getWidth(), getHeight(), QBrush(fillColor)); } if (model_->showDesktopNumber()) { painter->setFont(adjustFontSize(getWidth(), getHeight(), "0" /* reference string */, 0.5 /* scale factor */)); painter->setRenderHint(QPainter::TextAntialiasing); drawBorderedText(left_, top_, getWidth(), getHeight(), Qt::AlignCenter, QString::number(desktop_.number), 1 /* borderWidth */, Qt::black, Qt::white, painter); } // Draw the border for the current desktop. if (isCurrentDesktop()) { painter->setPen(model_->borderColor()); painter->drawRect(left_ - 1, top_ - 1, getWidth() + 1, getHeight() + 1); } } void DesktopSelector::mousePressEvent(QMouseEvent* e) { if (e->button() == Qt::LeftButton) { if (isCurrentDesktop()) { WindowSystem::setShowingDesktop(!WindowSystem::showingDesktop()); } else { WindowSystem::setCurrentDesktop(desktop_.id); } } else if (e->button() == Qt::RightButton) { // In case other DesktopSelectors have changed the config. showDesktopNumberAction_->setChecked(model_->showDesktopNumber()); showPopupMenu(&menu_); } } void DesktopSelector::loadConfig() { const auto& wallpaper = model_->wallpaper(desktop_.id, screen_); if (!wallpaper.isEmpty() && QFile::exists(wallpaper)) { setIconScaled(QPixmap(wallpaper)); hasCustomWallpaper_ = true; } showDesktopNumberAction_->setChecked(model_->showDesktopNumber()); } void DesktopSelector::saveConfig() { model_->setShowDesktopNumber(showDesktopNumberAction_->isChecked()); model_->saveAppearanceConfig(true /* repaintOnly */); } void DesktopSelector::setIconScaled(const QPixmap& icon) { if (icon.width() * desktopHeight_ != icon.height() * desktopWidth_) { QPixmap scaledIcon = icon.scaled(desktopWidth_, desktopHeight_); setIcon(scaledIcon); } else { setIcon(icon); } } void DesktopSelector::onDesktopNameChanged( std::string_view desktopId, std::string_view desktopName) { if (desktop_.id == desktopId) { setLabel(QString::fromStdString(std::string{desktopName})); titleAction_->setText(label_); } } void DesktopSelector::createMenu() { titleAction_ = menu_.addSection(label_); if (desktopEnv_->canSetWallpaper()) { menu_.addAction( QIcon::fromTheme("preferences-desktop-wallpaper"), QString("Set Wallpaper for Desktop ") + QString::number(desktop_.number), parent_, [this] { parent_->showWallpaperSettingsDialog(desktop_.number); }); } showDesktopNumberAction_ = menu_.addAction( QString("Show Desktop Number"), this, [this] { saveConfig(); }); showDesktopNumberAction_->setCheckable(true); menu_.addSeparator(); parent_->addPanelSettings(&menu_); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/desktop_selector.h000066400000000000000000000050121512233520000231700ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_DESKTOP_SELECTOR_H_ #define CRYSTALDOCK_DESKTOP_SELECTOR_H_ #include "icon_based_dock_item.h" #include #include #include #include #include #include "display/window_system.h" #include namespace crystaldock { // Pager icons. class DesktopSelector : public QObject, public IconBasedDockItem { Q_OBJECT public: DesktopSelector(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize, const VirtualDesktopInfo& desktop, int screen); virtual ~DesktopSelector() = default; int getWidthForSize(int size) const override { return isHorizontal() ? (size * desktopWidth_ / desktopHeight_) : size; } int getHeightForSize(int size) const override { return isHorizontal() ? size : (size * desktopHeight_ / desktopWidth_); } void draw(QPainter* painter) const override; void mousePressEvent(QMouseEvent* e) override; void loadConfig() override; // Sets the icon but scales the pixmap to the screen's width/height ratio. void setIconScaled(const QPixmap& icon); public slots: void onDesktopNameChanged(std::string_view desktopId, std::string_view desktopName); private: bool isCurrentDesktop() const { return WindowSystem::currentDesktop() == desktop_.id; } void createMenu(); void saveConfig(); DesktopEnv* desktopEnv_; VirtualDesktopInfo desktop_; // The screen that the parent panel is on, 0-based. int screen_; QSettings* config_; // Context (right-click) menu. QMenu menu_; QAction* titleAction_; QAction* showDesktopNumberAction_; int desktopWidth_; int desktopHeight_; bool hasCustomWallpaper_; }; } // namespace crystaldock #endif // CRYSTALDOCK_DESKTOP_SELECTOR_H_ dangvd-crystal-dock-7a34e96/src/view/dock_item.cc000066400000000000000000000017341512233520000217220ustar00rootroot00000000000000 /* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "dock_item.h" #include "dock_panel.h" namespace crystaldock { void DockItem::showPopupMenu(QMenu* menu) { parent_->setShowingPopup(true); menu->exec(parent_->mapToGlobal(QPoint(left_, top_))); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/dock_item.h000066400000000000000000000140561512233520000215650ustar00rootroot00000000000000 /* * This file is part of Crystal Dock. * Copyright (C) 2023 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_DOCK_ITEM_H_ #define CRYSTALDOCK_DOCK_ITEM_H_ #include #include #include #include #include #include #include namespace crystaldock { class DockPanel; // Base class for all dock items, e.g. launchers and pager icons. // // It's a design decision that DockItem is not a sub-class of QWidget, to make // the dock's parabolic zooming effect smoother. class DockItem { public: DockItem(DockPanel* parent, MultiDockModel* model, const QString& label, Qt::Orientation orientation, int minSize, int maxSize) : parent_(parent), model_(model), label_(label), orientation_(orientation), minSize_(minSize), maxSize_(maxSize), size_(minSize) {} virtual ~DockItem() {} // Gets the width of the item given a size. virtual int getWidthForSize(int size) const = 0; // Gets the height of the item given a size. virtual int getHeightForSize(int size) const = 0; // Draws itself on the parent's canvas. virtual void draw(QPainter* painter) const = 0; // Mouse press event handler. virtual void mousePressEvent(QMouseEvent* e) = 0; // Mouse wheel even handler. virtual void wheelEvent(QWheelEvent* e) {} // We manually reset active window on the dock's mouse event. // We don't want to always do this (e.g. handle this in window_system::state_change() handler) // because otherwise we wouldn't be able to click on an active window's icon to minimize it // (the click action would change the active window to be the dock). virtual void maybeResetActiveWindow(QMouseEvent* e) { WindowSystem::resetActiveWindow(); } // Some dock items (e.g. Application Menu or Clock) have their own global // (i.e. not dock-specific) config that they need to reload when the config // has been changed by another dock (not their parent dock). virtual void loadConfig() {} // Handles adding the task, e.g. for a Program dock item. virtual bool addTask(const WindowInfo* task) { return false; } // Handles updating the task, e.g. for a Program dock item. virtual bool updateTask(const WindowInfo* task) { return false; } // Handles removing the task, e.g. for a Program dock item. virtual bool removeTask(void* window) { return false; } // Does this (Program) dock item already have this task? virtual bool hasTask(void* window) { return false; } // Will this item be ordered before the Program item for this task? virtual bool beforeTask(const QString& program) { return true; } // Should be removed for example if a Program item has no task and is not pinned. virtual bool shouldBeRemoved() { return false; } // This is virtual so dynamic label can be implemented in its subclasses. virtual QString getLabel() const { return label_; } void setLabel(const QString& label) { label_ = label; } // For a Program dock item. virtual QString getAppId() const { return ""; } // For a Program dock item. virtual QString getAppLabel() const { return ""; } // For a Program dock item. virtual void updatePinnedStatus(bool pinned) {} // For a Program dock item. virtual void setDemandsAttention(bool demandsAttention) {} bool isHorizontal() const { return orientation_ == Qt::Horizontal; } void setAnimationStartAsCurrent() { startLeft_ = left_; startTop_ = top_; startSize_ = size_; } void setAnimationEndAsCurrent() { endLeft_ = left_; endTop_ = top_; endSize_ = size_; } void startAnimation(int numSteps) { left_ = startLeft_; top_ = startTop_; size_ = startSize_; currentStep_ = 0; numSteps_ = numSteps; } void nextAnimationStep() { ++currentStep_; if (currentStep_ <= numSteps_) { left_ = startLeft_ + (endLeft_ - startLeft_) * currentStep_ / numSteps_; top_ = startTop_ + (endTop_ - startTop_) * currentStep_ / numSteps_; size_ = startSize_ + (endSize_ - startSize_) * currentStep_ / numSteps_; } } // Gets max width, i.e. the width when the item is max zoomed. int getMaxWidth() const { return getWidthForSize(maxSize_); } // Gets max height, i.e. the width when the item is max zoomed. int getMaxHeight() const { return getHeightForSize(maxSize_); } // Gets min width, i.e. when the item is not zoomed in. int getMinWidth() const { return getWidthForSize(minSize_); } // Gets min height, i.e. when the item is not zoomed in. int getMinHeight() const { return getHeightForSize(minSize_); } int getWidth() const { return getWidthForSize(size_); } int getHeight() const { return getHeightForSize(size_); } protected: void showPopupMenu(QMenu* menu); DockPanel* parent_; MultiDockModel* model_; QString label_; // Label of the dock item. Qt::Orientation orientation_; // Orientation (horizontal/vertical). int minSize_; int maxSize_; int size_; int left_; int top_; // Center when minimized, as x or y depends on whether the orientation is // horizontal or vertical. This is used when calculating the size of the item // when the dock is in parabolic zoom. int minCenter_; // For animation. int startLeft_; int startTop_; int startSize_; int endLeft_; int endTop_; int endSize_; int currentStep_; int numSteps_; private: friend class DockPanel; }; } // namespace crystaldock #endif // CRYSTALDOCK_DOCK_ITEM_H_ dangvd-crystal-dock-7a34e96/src/view/dock_panel.cc000066400000000000000000001674651512233520000221010ustar00rootroot00000000000000 /* * This file is part of Crystal Dock. * Copyright (C) 2023 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "dock_panel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "add_panel_dialog.h" #include "application_menu.h" #include "battery_indicator.h" #include "clock.h" #include "desktop_selector.h" #include "keyboard_layout.h" #include "multi_dock_view.h" #include "program.h" #include "separator.h" #include "trash.h" #include "version_checker.h" #include "volume_control.h" #include "wifi_manager.h" #include #include #include namespace ranges = std::ranges; namespace crystaldock { /*static*/ constexpr char DockPanel::kVersion[] = "2.16"; DockPanel::DockPanel(MultiDockView* parent, MultiDockModel* model, int dockId) : QWidget(), parent_(parent), model_(model), dockId_(dockId), aboutDialog_(QMessageBox::Information, "About Crystal Dock", QString("

Crystal Dock ") + kVersion + "

" + "

Copyright (C) 2025 Viet Dang (dangvd@gmail.com)" + "

https://github.com/dangvd/crystal-dock" + "

License: GPLv3", QMessageBox::Ok, this, Qt::Tool), addPanelDialog_(this, model, dockId), appearanceSettingsDialog_(this, model), editKeyboardLayoutsDialog_(this, model), editLaunchersDialog_(this, model, dockId), applicationMenuSettingsDialog_(this, model), wallpaperSettingsDialog_(this, model), taskManagerSettingsDialog_(this, model), isMinimized_(true), isHidden_(false), isEntering_(false), isLeaving_(false), isAnimationActive_(false), isShowingPopup_(false), animationTimer_(std::make_unique(this)) { setAttribute(Qt::WA_TranslucentBackground); setWindowFlag(Qt::FramelessWindowHint); setMouseTracking(true); setAcceptDrops(true); createMenu(); loadDockConfig(); loadAppearanceConfig(); initUi(); connect(animationTimer_.get(), SIGNAL(timeout()), this, SLOT(updateAnimation())); connect(WindowSystem::self(), SIGNAL(numberOfDesktopsChanged(int)), this, SLOT(updatePager())); connect(WindowSystem::self(), SIGNAL(currentDesktopChanged(std::string_view)), this, SLOT(onCurrentDesktopChanged())); connect(WindowSystem::self(), SIGNAL(windowStateChanged(const WindowInfo*)), this, SLOT(onWindowStateChanged(const WindowInfo*))); connect(WindowSystem::self(), SIGNAL(windowTitleChanged(const WindowInfo*)), this, SLOT(onWindowTitleChanged(const WindowInfo*))); connect(WindowSystem::self(), SIGNAL(activeWindowChanged(void*)), this, SLOT(onActiveWindowChanged())); connect(WindowSystem::self(), SIGNAL(windowAdded(const WindowInfo*)), this, SLOT(onWindowAdded(const WindowInfo*))); connect(WindowSystem::self(), SIGNAL(windowRemoved(void*)), this, SLOT(onWindowRemoved(void*))); connect(WindowSystem::self(), SIGNAL(windowLeftCurrentDesktop(void*)), this, SLOT(onWindowLeftCurrentDesktop(void*))); connect(WindowSystem::self(), SIGNAL(windowLeftCurrentActivity(void*)), this, SLOT(onWindowLeftCurrentActivity(void*))); connect(WindowSystem::self(), SIGNAL(windowEnteredOutput(const WindowInfo*, const wl_output*)), this, SLOT(onWindowEnteredOutput(const WindowInfo*, const wl_output*))); connect(WindowSystem::self(), SIGNAL(windowLeftOutput(const WindowInfo*, const wl_output*)), this, SLOT(onWindowLeftOutput(const WindowInfo*, const wl_output*))); connect(WindowSystem::self(), SIGNAL(windowGeometryChanged(const WindowInfo*)), this, SLOT(onWindowGeometryChanged(const WindowInfo*))); connect(WindowSystem::self(), SIGNAL(currentActivityChanged(std::string_view)), this, SLOT(onCurrentActivityChanged())); connect(model_, SIGNAL(appearanceOutdated()), this, SLOT(update())); connect(model_, SIGNAL(appearanceChanged()), this, SLOT(reload())); connect(model_, SIGNAL(dockLaunchersChanged(int)), this, SLOT(onDockLaunchersChanged(int))); } void DockPanel::reload() { loadAppearanceConfig(); items_.clear(); initUi(); setMask(); } void DockPanel::refresh() { for (int i = 0; i < itemCount(); ++i) { if (items_[i]->shouldBeRemoved()) { items_.erase(items_.begin() + i); resizeTaskManager(); return; } } } void DockPanel::delayedRefresh() { QTimer::singleShot(100 /* msecs */, this, SLOT(refresh())); } void DockPanel::onCurrentDesktopChanged() { reloadTasks(); intellihideHideUnhide(); } void DockPanel::onCurrentActivityChanged() { reloadTasks(); intellihideHideUnhide(); } void DockPanel::setStrut() { switch(visibility_) { case PanelVisibility::AlwaysVisible: setStrut(isHorizontal() ? minHeight_ : minWidth_); break; case PanelVisibility::AutoHide: case PanelVisibility::IntelligentAutoHide: setStrut(WindowSystem::hasAutoHideManager() ? 0 : 1); break; default: setStrut(0); break; } } void DockPanel::togglePager() { showPager_ = !showPager_; reload(); saveDockConfig(); } void DockPanel::setScreen(int screen) { screen_ = screen; for (int i = 0; i < static_cast(screenActions_.size()); ++i) { screenActions_[i]->setChecked(i == screen); } screenGeometry_ = WindowSystem::screens()[screen]->geometry(); screenOutput_ = WindowSystem::getWlOutputForScreen(screen); WindowSystem::setScreen(this, screen); } void DockPanel::changeScreen(int screen) { if (screen_ == screen) { return; } model_->cloneDock(dockId_, position_, screen); deleteLater(); model_->removeDock(dockId_); } void DockPanel::updateAnimation() { for (const auto& item : items_) { item->nextAnimationStep(); } ++currentAnimationStep_; backgroundWidth_ = startBackgroundWidth_ + (endBackgroundWidth_ - startBackgroundWidth_) * currentAnimationStep_ / numAnimationSteps_; backgroundHeight_ = startBackgroundHeight_ + (endBackgroundHeight_ - startBackgroundHeight_) * currentAnimationStep_ / numAnimationSteps_; if (currentAnimationStep_ == numAnimationSteps_) { animationTimer_->stop(); isAnimationActive_ = false; if (isLeaving_) { isLeaving_ = false; updateLayout(); if (isHidden_ && !hasFocus()) { setAutoHide(); } } } repaint(); } void DockPanel::showOnlineDocumentation() { Program::launch( "xdg-open https://github.com/dangvd/crystal-dock/wiki/Documentation"); } void DockPanel::about() { aboutDialog_.exec(); } void DockPanel::showAppearanceSettingsDialog() { appearanceSettingsDialog_.reload(); appearanceSettingsDialog_.show(); } void DockPanel::showEditKeyboardLayoutsDialog() { editKeyboardLayoutsDialog_.refreshData(); editKeyboardLayoutsDialog_.show(); } void DockPanel::showEditLaunchersDialog() { editLaunchersDialog_.reload(); editLaunchersDialog_.show(); } void DockPanel::showApplicationMenuSettingsDialog() { applicationMenuSettingsDialog_.reload(); applicationMenuSettingsDialog_.show(); } void DockPanel::showWallpaperSettingsDialog(int desktop) { wallpaperSettingsDialog_.setFor(desktop, screen_); wallpaperSettingsDialog_.show(); } void DockPanel::showTaskManagerSettingsDialog() { taskManagerSettingsDialog_.reload(); taskManagerSettingsDialog_.show(); } void DockPanel::addDock() { addPanelDialog_.setMode(AddPanelDialog::Mode::Add); addPanelDialog_.show(); } void DockPanel::cloneDock() { addPanelDialog_.setMode(AddPanelDialog::Mode::Clone); addPanelDialog_.show(); } void DockPanel::removeDock() { if (model_->dockCount() == 1) { QMessageBox message(QMessageBox::Information, "Remove Panel", "The last panel cannot be removed.", QMessageBox::Ok, this, Qt::Tool); message.exec(); return; } QMessageBox question(QMessageBox::Question, "Remove Panel", "Do you really want to remove this panel?", QMessageBox::Yes | QMessageBox::No, this, Qt::Tool); if (question.exec() == QMessageBox::Yes) { deleteLater(); model_->removeDock(dockId_); } } void DockPanel::onWindowAdded(const WindowInfo* info) { intellihideHideUnhide(); if (autoHide() && !isHidden_) { setAutoHide(); } if (!showTaskManager()) { return; } if (isValidTask(info)) { if (addTask(info)) { resizeTaskManager(); } else { update(); } } } void DockPanel::onWindowRemoved(void* window) { intellihideHideUnhide(window); if (!showTaskManager()) { return; } removeTask(window); if (isEmpty()) { intellihideHideUnhide(); } } void DockPanel::onWindowLeftCurrentDesktop(void* window) { if (showTaskManager() && model_->currentDesktopTasksOnly()) { removeTask(window); } } void DockPanel::onWindowLeftCurrentActivity(void* window) { if (showTaskManager()) { removeTask(window); } } void DockPanel::onWindowGeometryChanged(const WindowInfo* task) { intellihideHideUnhide(); if (!showTaskManager()) { return; } if (!model_->currentScreenTasksOnly()) { return; } QRect windowGeometry(task->x, task->y, task->width, task->height); if (hasTask(task->window)) { if (!windowGeometry.intersects(screenGeometry_)) { removeTask(task->window); } } else { if (windowGeometry.intersects(screenGeometry_) && isValidTask(task)) { if (addTask(task)) { resizeTaskManager(); } } } } void DockPanel::onWindowStateChanged(const WindowInfo *task) { intellihideHideUnhide(); if (!showTaskManager()) { return; } for (auto& item : items_) { if (item->hasTask(task->window)) { item->setDemandsAttention(task->demandsAttention); return; } } } void DockPanel::onWindowTitleChanged(const WindowInfo *task) { if (model_->groupTasksByApplication()) { return; } for (auto& item : items_) { if (item->hasTask(task->window)) { item->setLabel(QString::fromStdString(task->title)); update(); return; } } } void DockPanel::onActiveWindowChanged() { update(); } void DockPanel::onWindowEnteredOutput(const WindowInfo* task, const wl_output* output) { intellihideHideUnhide(); if (!showTaskManager()) { return; } if (!model_->currentScreenTasksOnly()) { return; } if (screenOutput_ != output) { return; } if (addTask(task)) { resizeTaskManager(); } } void DockPanel::onWindowLeftOutput(const WindowInfo* task, const wl_output* output) { intellihideHideUnhide(); if (!showTaskManager()) { return; } if (!model_->currentScreenTasksOnly()) { return; } if (screenOutput_ != output) { return; } removeTask(task->window); } int DockPanel::taskIndicatorPos() { const auto margin = isGlass2D() || (is3D() && !isBottom()) ? kIndicatorMarginGlass2D : isFlat2D() ? kIndicatorSizeFlat2D : kIndicatorSizeMetal2D / 2; if (isHorizontal()) { int y = 0; if (is3D() && isBottom()) { y = maxHeight_ - k3DPanelThickness - 2; } else { // 2D if (isTop()) { y = itemSpacing_ / 3; } else { // bottom y = maxHeight_ - itemSpacing_ / 3 - margin; } } if (isFloating()) { if (isTop()) { y += floatingMargin_; } else { y -= floatingMargin_; } } return y; } else { // Vertical. int x = 0; if (isLeft()) { x = itemSpacing_ / 3; } else { // right x = maxWidth_ - itemSpacing_ / 3 - margin; } if (isFloating()) { if (isLeft()) { x += floatingMargin_; } else { x -= floatingMargin_; } } return x; } } int DockPanel::itemCount(const QString& appId) { const auto first = ranges::find_if( items_, [&appId](auto& item) { return appId == item->getAppId(); }); if (first == items_.end()) { return 0; } const auto last = ranges::find_if( first, items_.end(), [&appId](auto& item) { return appId != item->getAppId(); }); return last - first; } void DockPanel::updatePinnedStatus(const QString& appId, bool pinned) { const auto first = ranges::find_if( items_, [&appId](auto& item) { return appId == item->getAppId(); }); if (first == items_.end()) { return; } const auto last = ranges::find_if( first, items_.end(), [&appId](auto& item) { return appId != item->getAppId(); }); ranges::for_each(first, last, [pinned](auto& item) { item->updatePinnedStatus(pinned); }); } void DockPanel::setShowingPopup(bool showingPopup) { isShowingPopup_ = showingPopup; if (!isShowingPopup_) { // We have to do these complicated workarounds because QCursor::pos() does not // exactly return the current mouse position but it depends on related mouse events. auto mousePosition = mapFromGlobal(QCursor::pos()); int x = 0; int y = 0; int w = 0; int h = 0; int x2 = 0; int y2 = 0; int w2 = 0; int h2 = 0; int itemCount = static_cast(items_.size()); switch (position_) { case PanelPosition::Top: x = itemSpacing_; w = maxWidth_ - 2 * x; y = itemSpacing_ + (isFloating() ? floatingMargin_ : 0); h = minSize_; if (activeItem_ >= 0 && activeItem_ < itemCount) { x2 = items_[activeItem_]->left_; w2 = items_[activeItem_]->getMaxWidth(); y2 = y; h2 = maxSize_; } break; case PanelPosition::Bottom: x = itemSpacing_ + (is3D() && isBottom() ? margin3D_ : 0); w = maxWidth_ - 2 * x; y = maxHeight_ - itemSpacing_ - (isFloating() ? floatingMargin_ : 0) - (is3D() && isBottom() ? k3DPanelThickness : 0) - minSize_; h = minSize_; if (activeItem_ >= 0 && activeItem_ < itemCount) { x2 = items_[activeItem_]->left_; w2 = items_[activeItem_]->getMaxWidth(); y2 = y + minSize_ - maxSize_; h2 = maxSize_; } break; case PanelPosition::Left: y = itemSpacing_; h = maxHeight_ - 2 * y; x = itemSpacing_ + (isFloating() ? floatingMargin_ : 0); w = minSize_; if (activeItem_ >= 0 && activeItem_ < itemCount) { y2 = items_[activeItem_]->top_; h2 = items_[activeItem_]->getMaxHeight(); x2 = y; w2 = maxSize_; } break; case PanelPosition::Right: y = itemSpacing_; h = maxHeight_ - 2 * y; x = maxWidth_ - itemSpacing_ - (isFloating() ? floatingMargin_ : 0) - minSize_; w = minSize_; if (activeItem_ >= 0 && activeItem_ < itemCount) { y2 = items_[activeItem_]->top_; h2 = items_[activeItem_]->getMaxHeight(); x2 = x + minSize_ - maxSize_; w2 = maxSize_; } break; } QRect rect(x, y, w, h); QRect rect2(x2, y2, w2, h2); if (!rect.contains(mousePosition) && !rect2.contains(mousePosition)) { leaveEvent(nullptr); } } } void DockPanel::paintEvent(QPaintEvent* e) { if (!WindowSystem::hasAutoHideManager() && isHidden_ && (autoHide() || intellihide())) { return; } QPainter painter(this); if (is3D()) { drawGlass3D(painter); } else { draw2D(painter); } drawTooltip(painter); } void DockPanel::drawGlass3D(QPainter& painter) { if (isHorizontal()) { int y = isTop() ? isFloating() ? floatingMargin_ : 0 : isFloating() ? maxHeight_ - backgroundHeight_ - floatingMargin_ : maxHeight_ - backgroundHeight_; if (isBottom()) { // 3D styles only apply to bottom dock. y -= k3DPanelThickness; draw3dDockPanel( (maxWidth_ - backgroundWidth_) / 2, y, backgroundWidth_ - 1, backgroundHeight_ - 1, backgroundHeight_ / 16, borderColor_, backgroundColor_, &painter); } else { fillRoundedRect( (maxWidth_ - backgroundWidth_) / 2, y, backgroundWidth_ - 1, backgroundHeight_ - 1, backgroundHeight_ / 16, /*showBorder=*/true, borderColor_, backgroundColor_, &painter); } } else { // Vertical const int x = isLeft() ? isFloating() ? floatingMargin_ : 0 : isFloating() ? maxWidth_ - backgroundWidth_ - floatingMargin_ : maxWidth_ - backgroundWidth_; fillRoundedRect(x, (maxHeight_ - backgroundHeight_) / 2, backgroundWidth_ - 1, backgroundHeight_ - 1, backgroundWidth_ / 16, /*showBorder=*/true, borderColor_, backgroundColor_, &painter); } if (isBottom()) { QImage mainImage(width(), height(), QImage::Format_ARGB32); mainImage.fill(0); QPainter mainPainter(&mainImage); // Draw the items from the end to avoid zoomed items getting clipped by // non-zoomed items. for (int i = itemCount() - 1; i >= 0; --i) { items_[i]->draw(&mainPainter); } painter.drawImage(0, 0, mainImage); int y = height() - itemSpacing_ - k3DPanelThickness; if (isFloating()) { y -= floatingMargin_; } QImage toMirrorImage = mainImage.copy(0, y - itemSpacing_ + 2, width(), itemSpacing_ - 2); QImage mirrorImage = toMirrorImage.flipped(Qt::Vertical); painter.setOpacity(0.3); painter.drawImage(0, y, mirrorImage); painter.setOpacity(1.0); } else { // Draw the items from the end to avoid zoomed items getting clipped by // non-zoomed items. for (int i = itemCount() - 1; i >= 0; --i) { items_[i]->draw(&painter); } } } void DockPanel::draw2D(QPainter& painter) { const QColor bgColor = isGlass2D() ? model_->backgroundColor() : isFlat2D() ? model_->backgroundColor2D() : model_->backgroundColorMetal2D(); const auto showBorder = isGlass2D() || isMetal2D(); const QColor borderColor = isGlass2D() ? model_->borderColor() : model_->borderColorMetal2D(); if (isHorizontal()) { const int y = isTop() ? isFloating() ? floatingMargin_ : 0 : isFloating() ? maxHeight_ - backgroundHeight_ - floatingMargin_ : maxHeight_ - backgroundHeight_; const int r = isGlass2D() ? backgroundHeight_ / 16 : isFlat2D() ? backgroundHeight_ / 4 : 0; fillRoundedRect( (maxWidth_ - backgroundWidth_) / 2, y, backgroundWidth_ - 1, backgroundHeight_ - 1, r, showBorder, borderColor, bgColor, &painter); } else { // Vertical const int x = isLeft() ? isFloating() ? floatingMargin_ : 0 : isFloating() ? maxWidth_ - backgroundWidth_ - floatingMargin_ : maxWidth_ - backgroundWidth_; const int r = isGlass2D() ? backgroundWidth_ / 16 : isFlat2D() ? backgroundWidth_ / 4 : 0; fillRoundedRect( x, (maxHeight_ - backgroundHeight_) / 2, backgroundWidth_ - 1, backgroundHeight_ - 1, r, showBorder, borderColor, bgColor, &painter); } // Draw the items from the end to avoid zoomed items getting clipped by // non-zoomed items. for (int i = itemCount() - 1; i >= 0; --i) { items_[i]->draw(&painter); } } void DockPanel::drawTooltip(QPainter& painter) { if (model_->showTooltip() && !isAnimationActive_ && activeItem_ >= 0 && activeItem_ < static_cast(items_.size())) { if (isHorizontal()) { const auto& item = items_[activeItem_]; QFont font; font.setPointSize(model_->tooltipFontSize()); font.setBold(true); QFontMetrics metrics(font); const auto tooltipWidth = metrics.boundingRect(item->getLabel()).width(); painter.setFont(font); int x = item->left_ + item->getWidth() / 2 - tooltipWidth / 2; x = std::min(x, maxWidth_ - tooltipWidth); x = std::max(x, 0); int y = isTop() ? maxHeight_ - tooltipSize_ / 2 : tooltipSize_ * 3 / 4; drawBorderedText(x, y, item->getLabel(), /*borderWidth*/ 2, Qt::black, Qt::white, &painter); } else { // Vertical // Do not draw tooltip for Vertical positions for now because the total // area of the dock would take too much desktop space. } } } void DockPanel::mouseMoveEvent(QMouseEvent* e) { const auto x = e->position().x(); const auto y = e->position().y(); if (isEntering_) { // Don't do the parabolic zooming if the mouse is outside the minimized area. // Also don't do the parabolic zooming if the mouse is near the border. // Quite often the user was just scrolling a window etc. if (!checkMouseEnter(x, y)) { return; } } if (isAnimationActive_) { return; } updateLayout(x, y); } bool DockPanel::checkMouseEnter(int x, int y) { int x0, y0; if (position_ == PanelPosition::Bottom) { if (!WindowSystem::hasAutoHideManager() && visibility_ == PanelVisibility::AutoHide) { y0 = maxHeight_ - 1; } else { y0 = maxHeight_ - minHeight_; if (isFloating()) { y0 += floatingMargin_; } } if (y < y0) { return false; } } else if (position_ == PanelPosition::Top) { if (!WindowSystem::hasAutoHideManager() && visibility_ == PanelVisibility::AutoHide) { y0 = 1; } else { y0 = minHeight_; if (isFloating()) { y0 -= floatingMargin_; } } if (y > y0) { return false; } } else if (position_ == PanelPosition::Left) { if (!WindowSystem::hasAutoHideManager() && visibility_ == PanelVisibility::AutoHide) { x0 = 1; } else { x0 = minWidth_; if (isFloating()) { x0 -= floatingMargin_; } } if (x > x0) { return false; } } else { // Right if (!WindowSystem::hasAutoHideManager() && visibility_ == PanelVisibility::AutoHide) { x0 = maxWidth_ - 1; } else { x0 = maxWidth_ - minWidth_; if (isFloating()) { x0 += floatingMargin_; } } if (x < x0) { return false; } } if (isHorizontal() && (x < (maxWidth_ - minWidth_) / 2 || x > (maxWidth_ + minWidth_) / 2)) { return false; } if (!isHorizontal() && (y < (maxHeight_ - minHeight_) / 2 || y > (maxHeight_ + minHeight_) / 2)) { return false; } return true; } bool DockPanel::intellihideShouldHide(void* excluding_window) { if (visibility_ != PanelVisibility::IntelligentAutoHide) { return false; } if (!isMinimized_) { return false; } if (isEmpty()) { return true; } // For tiling compositors, we only show the dock if there's no window. if (DesktopEnv::getDesktopEnv()->isTiling()) { for (const auto* task : WindowSystem::windows()) { if (shouldConsiderTaskForIntellihide(task) && (!excluding_window || task->window != excluding_window)) { return true; } } return false; } // For stacking compositors, we hide the dock if there's a maximized/fullscreen window. // If the compositor emits window geometry event, we also hide the dock if there's // a window that overlaps the dock. QRect dockGeometry = getMinimizedDockGeometry(); for (const auto* task : WindowSystem::windows()) { if (shouldConsiderTaskForIntellihide(task) && (!excluding_window || task->window != excluding_window)) { if ((task->maximized || task->fullscreen) && task->outputs.contains(screenOutput_)) { return true; } QRect windowGeometry(task->x, task->y, task->width, task->height); if (windowGeometry.isValid() && !task->minimized && windowGeometry.intersects(dockGeometry)) { return true; } } } return false; } void DockPanel::intellihideHideUnhide(void* excluding_window) { if (visibility_ != PanelVisibility::IntelligentAutoHide) { return; } if (intellihideShouldHide(excluding_window)) { if (!isHidden_ && isMinimized_) { setAutoHide(); } } else { if (isHidden_) { setAutoHide(false); } } } bool DockPanel::isEmpty() { for (const auto& item : items_) { if (item->getAppId() != kSeparatorId && item->getAppId() != kLauncherSeparatorId) { return false; } } return true; } void DockPanel::mousePressEvent(QMouseEvent* e) { if (isAnimationActive_) { return; } if (activeItem_ >= 0 && activeItem_ < static_cast(items_.size())) { items_[activeItem_]->maybeResetActiveWindow(e); items_[activeItem_]->mousePressEvent(e); } } void DockPanel::wheelEvent(QWheelEvent* e) { if (isAnimationActive_) { return; } if (activeItem_ >= 0 && activeItem_ < static_cast(items_.size())) { items_[activeItem_]->wheelEvent(e); } } void DockPanel::enterEvent (QEnterEvent* e) { if (isMinimized_) { isEntering_ = true; } } void DockPanel::leaveEvent(QEvent* e) { if (isMinimized_ || isShowingPopup_) { return; } isLeaving_ = true; updateLayout(); activeItem_ = -1; } void DockPanel::dragEnterEvent(QDragEnterEvent* e) { if (e->mimeData()->hasUrls()) { e->acceptProposedAction(); for (const auto& item : items_) { Trash* trash = dynamic_cast(item.get()); if (trash) { trash->setAcceptDrops(true); } } } } void DockPanel::dragMoveEvent(QDragMoveEvent* e) { if (e->mimeData()->hasUrls()) { e->acceptProposedAction(); } } void DockPanel::dropEvent(QDropEvent* e) { for (const auto& item : items_) { Trash* trash = dynamic_cast(item.get()); if (trash) { trash->setAcceptDrops(false); trash->dropEvent(e); return; } } } void DockPanel::initUi() { initApplicationMenu(); initPager(); initLaunchers(); initTasks(); initTrash(); initWifiManager(); initVolumeControl(); initBatteryIndicator(); initKeyboardLayout(); initVersionChecker(); initClock(); initLayoutVars(); updateLayout(); setStrut(); if (WindowSystem::hasAutoHideManager() && intellihideShouldHide()) { setAutoHide(); } } void DockPanel::addPanelSettings(QMenu* menu) { for (const auto& action : menu_.actions()) { menu->addAction(action); } } void DockPanel::createMenu() { QMenu* panelMenu = menu_.addMenu("&Panel"); panelMenu->addAction(QIcon::fromTheme("list-add"), QString("&Add Panel"), this, SLOT(addDock())); panelMenu->addAction(QIcon::fromTheme("edit-copy"), QString("&Clone Panel"), this, SLOT(cloneDock())); panelMenu->addAction(QIcon::fromTheme("edit-delete"), QString("&Remove Panel"), this, SLOT(removeDock())); panelMenu->addSeparator(); QMenu* extraComponents = panelMenu->addMenu(QString("&Optional Features")); applicationMenuAction_ = extraComponents->addAction(QString("Application Menu"), this, SLOT(toggleApplicationMenu())); applicationMenuAction_->setCheckable(true); pagerAction_ = extraComponents->addAction(QString("Pager"), this, SLOT(togglePager())); pagerAction_->setCheckable(true); taskManagerAction_ = extraComponents->addAction(QString("Task Manager"), this, SLOT(toggleTaskManager())); taskManagerAction_->setCheckable(true); trashAction_ = extraComponents->addAction(QString("Trash"), this, SLOT(toggleTrash())); trashAction_->setCheckable(true); wifiManagerAction_ = extraComponents->addAction(QString("Wi-Fi Manager"), this, SLOT(toggleWifiManager())); wifiManagerAction_->setCheckable(true); volumeControlAction_ = extraComponents->addAction(QString("Volume Control"), this, SLOT(toggleVolumeControl())); volumeControlAction_->setCheckable(true); batteryIndicatorAction_ = extraComponents->addAction(QString("Battery Indicator"), this, SLOT(toggleBatteryIndicator())); batteryIndicatorAction_->setCheckable(true); keyboardLayoutAction_ = extraComponents->addAction(QString("Keyboard Layout"), this, SLOT(toggleKeyboardLayout())); keyboardLayoutAction_->setCheckable(true); versionCheckerAction_ = extraComponents->addAction(QString("Version Checker"), this, SLOT(toggleVersionChecker())); versionCheckerAction_->setCheckable(true); clockAction_ = extraComponents->addAction(QString("Clock"), this, SLOT(toggleClock())); clockAction_->setCheckable(true); QMenu* position = panelMenu->addMenu(QString("&Position")); positionTop_ = position->addAction(QString("&Top"), this, [this]() { updatePosition(PanelPosition::Top); }); positionTop_->setCheckable(true); positionBottom_ = position->addAction(QString("&Bottom"), this, [this]() { updatePosition(PanelPosition::Bottom); }); positionBottom_->setCheckable(true); positionLeft_ = position->addAction(QString("&Left"), this, [this]() { updatePosition(PanelPosition::Left); }); positionLeft_->setCheckable(true); positionRight_ = position->addAction(QString("&Right"), this, [this]() { updatePosition(PanelPosition::Right); }); positionRight_->setCheckable(true); const int numScreens = WindowSystem::screens().size(); if (numScreens > 1) { QMenu* screen = panelMenu->addMenu(QString("Scr&een")); for (int i = 0; i < numScreens; ++i) { QAction* action = screen->addAction( "Screen " + QString::number(i + 1), this, [this, i]() { changeScreen(i); }); action->setCheckable(true); screenActions_.push_back(action); } } QMenu* visibility = panelMenu->addMenu(QString("&Visibility")); visibilityAlwaysVisibleAction_ = visibility->addAction( QString("Always &Visible"), this, [this]() { updateVisibility(PanelVisibility::AlwaysVisible); }); visibilityAlwaysVisibleAction_->setCheckable(true); visibilityIntelligentAutoHideAction_ = visibility->addAction( QString("&Intelligent Auto Hide"), this, [this]() { updateVisibility(PanelVisibility::IntelligentAutoHide); }); visibilityIntelligentAutoHideAction_->setCheckable(true); visibilityAutoHideAction_ = visibility->addAction( QString("Auto &Hide"), this, [this]() { updateVisibility(PanelVisibility::AutoHide); }); visibilityAutoHideAction_->setCheckable(true); visibilityAlwaysOnTopAction_ = visibility->addAction( QString("Always On &Top"), this, [this]() { updateVisibility(PanelVisibility::AlwaysOnTop); }); visibilityAlwaysOnTopAction_->setCheckable(true); panelMenu->addSeparator(); panelMenu->addAction(QIcon::fromTheme("application-exit"), "E&xit", parent_, SLOT(exit())); QMenu* appearanceMenu = menu_.addMenu("&Appearance"); appearanceMenu->addAction( QIcon::fromTheme("configure"), QString("Appearance &Settings"), this, [this] { minimize(); QTimer::singleShot(DockPanel::kExecutionDelayMs, [this]{ showAppearanceSettingsDialog(); }); }); appearanceMenu->addSeparator(); floatingStyleAction_ = appearanceMenu->addAction( QString("Floating Panel"), this, [this] { changeFloatingStyle(); }); floatingStyleAction_->setCheckable(true); floatingStyleAction_->setChecked(isFloating()); glass3DStyleAction_ = appearanceMenu->addAction( QString("Style: Glass 3D"), this, [this] { changePanelStyle(isFloating() ? PanelStyle::Glass3D_Floating : PanelStyle::Glass3D_NonFloating); }); glass3DStyleAction_->setCheckable(true); glass2DStyleAction_ = appearanceMenu->addAction( QString("Style: Glass 2D"), this, [this] { changePanelStyle(isFloating() ? PanelStyle::Glass2D_Floating : PanelStyle::Glass2D_NonFloating); }); glass2DStyleAction_->setCheckable(true); flat2DStyleAction_ = appearanceMenu->addAction( QString("Style: Flat 2D"), this, [this] { changePanelStyle(isFloating() ? PanelStyle::Flat2D_Floating : PanelStyle::Flat2D_NonFloating); }); flat2DStyleAction_->setCheckable(true); metal2DStyleAction_ = appearanceMenu->addAction( QString("Style: Metal 2D"), this, [this] { changePanelStyle(isFloating() ? PanelStyle::Metal2D_Floating : PanelStyle::Metal2D_NonFloating); }); metal2DStyleAction_->setCheckable(true); QMenu* helpMenu = menu_.addMenu(QIcon::fromTheme("help-contents"), "&Help"); helpMenu->addAction(QIcon::fromTheme("help-contents"), QString("Online &Documentation"), this, SLOT(showOnlineDocumentation())); helpMenu->addAction(QIcon::fromTheme("help-about"), QString("A&bout Crystal Dock"), this, [this] { minimize(); QTimer::singleShot(DockPanel::kExecutionDelayMs, [this]{ about(); }); }); } void DockPanel::setPosition(PanelPosition position) { position_ = position; orientation_ = (position_ == PanelPosition::Top || position_ == PanelPosition::Bottom) ? Qt::Horizontal : Qt::Vertical; positionTop_->setChecked(position == PanelPosition::Top); positionBottom_->setChecked(position == PanelPosition::Bottom); positionLeft_->setChecked(position == PanelPosition::Left); positionRight_->setChecked(position == PanelPosition::Right); } void DockPanel::setVisibility(PanelVisibility visibility) { visibility_ = visibility; visibilityAlwaysVisibleAction_->setChecked( visibility_ == PanelVisibility::AlwaysVisible); visibilityIntelligentAutoHideAction_->setChecked( visibility_ == PanelVisibility::IntelligentAutoHide); visibilityAutoHideAction_->setChecked( visibility_ == PanelVisibility::AutoHide); visibilityAlwaysOnTopAction_->setChecked( visibility_ == PanelVisibility::AlwaysOnTop); } void DockPanel::setPanelStyle(PanelStyle panelStyle) { panelStyle_ = panelStyle; floatingStyleAction_->setChecked(isFloating()); glass3DStyleAction_->setChecked(is3D()); glass2DStyleAction_->setChecked(isGlass2D()); flat2DStyleAction_->setChecked(isFlat2D()); metal2DStyleAction_->setChecked(isMetal2D()); } void DockPanel::loadDockConfig() { setPosition(model_->panelPosition(dockId_)); setScreen(model_->screen(dockId_)); setVisibility(model_->visibility(dockId_)); showApplicationMenu_ = model_->showApplicationMenu(dockId_); applicationMenuAction_->setChecked(showApplicationMenu_); showPager_ = model_->showPager(dockId_) && WindowSystem::hasVirtualDesktopManager(); pagerAction_->setVisible(WindowSystem::hasVirtualDesktopManager()); pagerAction_->setChecked(showPager_); taskManagerAction_->setChecked(model_->showTaskManager(dockId_)); showTrash_ = model_->showTrash(dockId_); trashAction_->setChecked(showTrash_); showWifiManager_ = model_->showWifiManager(dockId_); wifiManagerAction_->setChecked(showWifiManager_); showVolumeControl_ = model_->showVolumeControl(dockId_); volumeControlAction_->setChecked(showVolumeControl_); showBatteryIndicator_ = model_->showBatteryIndicator(dockId_); batteryIndicatorAction_->setChecked(showBatteryIndicator_); showKeyboardLayout_ = model_->showKeyboardLayout(dockId_); keyboardLayoutAction_->setChecked(showKeyboardLayout_); showVersionChecker_ = model_->showVersionChecker(dockId_); versionCheckerAction_->setChecked(showVersionChecker_); showClock_ = model_->showClock(dockId_); clockAction_->setChecked(showClock_); } void DockPanel::saveDockConfig() { model_->setPanelPosition(dockId_, position_); model_->setScreen(dockId_, screen_); model_->setVisibility(dockId_, visibility_); model_->setShowApplicationMenu(dockId_, showApplicationMenu_); model_->setShowPager(dockId_, showPager_); model_->setShowTaskManager(dockId_, taskManagerAction_->isChecked()); model_->setShowTrash(dockId_, showTrash_); model_->setShowWifiManager(dockId_, showWifiManager_); model_->setShowVolumeControl(dockId_, showVolumeControl_); model_->setShowBatteryIndicator(dockId_, showBatteryIndicator_); model_->setShowKeyboardLayout(dockId_, showKeyboardLayout_); model_->setShowVersionChecker(dockId_, showVersionChecker_); model_->setShowClock(dockId_, showClock_); model_->saveDockConfig(dockId_); } void DockPanel::loadAppearanceConfig() { minSize_ = model_->minIconSize(); maxSize_ = model_->maxIconSize(); spacingFactor_ = model_->spacingFactor(); backgroundColor_ = model_->backgroundColor(); borderColor_ = model_->borderColor(); tooltipFontSize_ = model_->tooltipFontSize(); setPanelStyle(model_->panelStyle()); } void DockPanel::initApplicationMenu() { if (showApplicationMenu_) { items_.push_back(std::make_unique( this, model_, orientation_, minSize_, maxSize_)); } } void DockPanel::initLaunchers() { for (const auto& launcherConfig : model_->launcherConfigs(dockId_)) { if (launcherConfig.appId == kSeparatorId || launcherConfig.appId == kLauncherSeparatorId) { items_.push_back(std::make_unique( this, model_, orientation_, minSize_, maxSize_, launcherConfig.appId == kLauncherSeparatorId)); } else { QPixmap icon = loadIcon(launcherConfig.icon, kIconLoadSize); items_.push_back(std::make_unique( this, model_, launcherConfig.appId, launcherConfig.name, orientation_, icon, minSize_, maxSize_, launcherConfig.command, model_->isAppMenuEntry(launcherConfig.appId.toStdString()), /*pinned=*/true)); } } } void DockPanel::initPager() { if (showPager_) { for (const auto& desktop : WindowSystem::desktops()) { items_.push_back(std::make_unique( this, model_, orientation_, minSize_, maxSize_, desktop, screen_)); } } } void DockPanel::initTasks() { if (!showTaskManager()) { return; } for (const auto* task : WindowSystem::windows()) { if (isValidTask(task)) { addTask(task); } } } void DockPanel::reloadTasks() { if (!showTaskManager()) { return; } const int itemsToKeep = applicationMenuItemCount() + pagerItemCount(); items_.resize(itemsToKeep); initLaunchers(); initTasks(); initTrash(); initWifiManager(); initVolumeControl(); initBatteryIndicator(); initKeyboardLayout(); initVersionChecker(); initClock(); resizeTaskManager(); if (WindowSystem::hasAutoHideManager() && intellihideShouldHide()) { setAutoHide(); } } bool DockPanel::addTask(const WindowInfo* task) { // Checks is the task already exists. if (hasTask(task->window)) { return false; } // Tries adding the task to existing programs. for (auto& item : items_) { if (item->addTask(task)) { return false; } } // Adds a new program. auto app = model_->findApplication(task->appId); if (!app && !task->appId.empty()) { std::cerr << "Could not find application with id: " << task->appId << ". The window icon will have limited functionalities." << std::endl; } const QString label = app ? app->name : QString::fromStdString(task->title); const QString appId = app ? app->appId : QString::fromStdString(task->appId); QPixmap appIcon = app ? loadIcon(app->icon, kIconLoadSize) : QPixmap(); QString taskIconName = QString::fromStdString(task->icon); QPixmap taskIcon = appIcon.isNull() && !taskIconName.isEmpty() ? loadIcon(taskIconName, kIconLoadSize) : QPixmap(); if (app && appIcon.isNull()) { std::cerr << "Could not find icon with name: " << app->icon.toStdString() << " in the current icon theme and its fallbacks." << " The window icon will have limited functionalities." << std::endl; } int i = 0; for (; i < itemCount() && items_[i]->beforeTask(label); ++i); if (!model_->groupTasksByApplication()) { for (; i < itemCount() && items_[i]->getAppLabel() == label; ++i); } if (!appIcon.isNull()) { const auto pinned = !model_->groupTasksByApplication() && model_->launchers(dockId_).contains(app->appId); items_.insert(items_.begin() + i, std::make_unique( this, model_, appId, label, orientation_, appIcon, minSize_, maxSize_, app->command, /*isAppMenuEntry=*/true, pinned)); } else if (!taskIcon.isNull()) { items_.insert(items_.begin() + i, std::make_unique( this, model_, appId, label, orientation_, taskIcon, minSize_, maxSize_)); } else { items_.insert(items_.begin() + i, std::make_unique( this, model_, appId, label, orientation_, QPixmap(), minSize_, maxSize_)); } items_[i]->addTask(task); return true; } void DockPanel::removeTask(void* window) { for (int i = 0; i < itemCount(); ++i) { if (items_[i]->removeTask(window)) { if (items_[i]->shouldBeRemoved()) { items_.erase(items_.begin() + i); resizeTaskManager(); } return; } } } void DockPanel::updateTask(const WindowInfo* task) { for (auto& item : items_) { if (item->updateTask(task)) { return; } } } bool DockPanel::isValidTask(const WindowInfo* task) { if (task == nullptr) { return false; } if (task->skipTaskbar) { return false; } if (WindowSystem::hasVirtualDesktopManager() && model_->currentDesktopTasksOnly() && !task->onAllDesktops && task->desktop != WindowSystem::currentDesktop()) { return false; } if (model_->currentScreenTasksOnly()) { if (!task->outputs.empty() && !task->outputs.contains(screenOutput_)) { return false; } QRect taskGeometry(task->x, task->y, task->width, task->height); if (taskGeometry.isValid() && !screenGeometry_.intersects(taskGeometry)) { return false; } } if (WindowSystem::hasActivityManager() && !WindowSystem::currentActivity().empty() && !task->activity.empty() && task->activity != WindowSystem::currentActivity()) { return false; } return true; } bool DockPanel::shouldConsiderTaskForIntellihide(const WindowInfo* task) { if (task == nullptr) { return false; } if (WindowSystem::hasVirtualDesktopManager() && !task->onAllDesktops && task->desktop != WindowSystem::currentDesktop()) { return false; } QRect taskGeometry(task->x, task->y, task->width, task->height); if (taskGeometry.isValid() && !screenGeometry_.intersects(taskGeometry)) { return false; } if (WindowSystem::hasActivityManager() && !WindowSystem::currentActivity().empty() && !task->activity.empty() && task->activity != WindowSystem::currentActivity()) { return false; } return true; } bool DockPanel::hasTask(void* window) { for (auto& item : items_) { if (item->hasTask(window)) { return true; } } return false; } void DockPanel::initTrash() { if (showTrash_) { items_.push_back(std::make_unique( this, model_, orientation_, minSize_, maxSize_)); } } void DockPanel::initWifiManager() { if (showWifiManager_) { items_.push_back(std::make_unique( this, model_, orientation_, minSize_, maxSize_)); } } void DockPanel::initVolumeControl() { if (showVolumeControl_) { items_.push_back(std::make_unique( this, model_, orientation_, minSize_, maxSize_)); } } void DockPanel::initBatteryIndicator() { if (showBatteryIndicator_) { items_.push_back(std::make_unique( this, model_, orientation_, minSize_, maxSize_)); } } void DockPanel::initKeyboardLayout() { if (showKeyboardLayout_) { items_.push_back(std::make_unique( this, model_, orientation_, minSize_, maxSize_)); } } void DockPanel::initVersionChecker() { if (showVersionChecker_) { items_.push_back(std::make_unique( this, model_, orientation_, minSize_, maxSize_)); } } void DockPanel::initClock() { if (showClock_) { items_.push_back(std::make_unique( this, model_, orientation_, minSize_, maxSize_)); } } void DockPanel::initLayoutVars() { const auto spacingMultiplier = isMetal2D() ? kSpacingMultiplierMetal2D : kSpacingMultiplier; itemSpacing_ = std::round(minSize_* spacingMultiplier * spacingFactor_); margin3D_ = static_cast(minSize_ * 0.6); floatingMargin_ = model_->floatingMargin(); parabolicMaxX_ = std::round(2.5 * (minSize_ + itemSpacing_)); numAnimationSteps_ = 14; QFont font; font.setPointSize(model_->tooltipFontSize()); font.setBold(true); QFontMetrics metrics(font); tooltipSize_ = metrics.boundingRect("Tooltip").height(); const int distance = minSize_ + itemSpacing_; // The difference between minWidth_ and maxWidth_ // (horizontal mode) or between minHeight_ and // maxHeight_ (vertical mode). int delta = 0; if (itemCount() >= 5) { delta = parabolic(0) + 2 * parabolic(distance) + 2 * parabolic(2 * distance) - 5 * minSize_; } else if (itemCount() == 4) { delta = parabolic(0) + 2 * parabolic(distance) + parabolic(2 * distance) - 4 * minSize_; } else if (itemCount() == 3) { delta = parabolic(0) + 2 * parabolic(distance) - 3 * minSize_; } else if (itemCount() == 2) { delta = parabolic(0) + parabolic(distance) - 2 * minSize_; } else if (itemCount() == 1) { delta = parabolic(0) - minSize_; } if (orientation_ == Qt::Horizontal) { minWidth_ = itemSpacing_; if (isBottom() && is3D()) { minWidth_ += 2 * margin3D_; } for (const auto& item : items_) { minWidth_ += (item->getMinWidth() + itemSpacing_); } minBackgroundWidth_ = minWidth_; minHeight_ = minSize_ + 2 * itemSpacing_; minBackgroundHeight_ = minHeight_; maxWidth_ = minWidth_ + delta; maxHeight_ = 2 * itemSpacing_ + maxSize_ + tooltipSize_; if (isFloating()) { maxHeight_ += 2 * floatingMargin_; minHeight_ += 2 * floatingMargin_; } if (is3D() && isBottom()) { maxHeight_ += k3DPanelThickness; minHeight_ += k3DPanelThickness; } } else { // Vertical minHeight_ = itemSpacing_; for (const auto& item : items_) { minHeight_ += (item->getMinHeight() + itemSpacing_); } minBackgroundHeight_ = minHeight_; minWidth_ = minSize_ + 2 * itemSpacing_; minBackgroundWidth_ = minWidth_; maxHeight_ = minHeight_ + delta; maxWidth_ = 2 * itemSpacing_ + maxSize_ + tooltipSize_; if (isFloating()) { maxWidth_ += 2 * floatingMargin_; minWidth_ += 2 * floatingMargin_; } } resize(maxWidth_, maxHeight_); } QRect DockPanel::getMinimizedDockGeometry() { QRect dockGeometry; dockGeometry.setX( isHorizontal() ? screenGeometry_.x() + (screenGeometry_.width() - minWidth_) / 2 : isLeft() ? screenGeometry_.x() : screenGeometry_.x() + screenGeometry_.width() - minWidth_); dockGeometry.setY( isHorizontal() ? isTop() ? screenGeometry_.y() : screenGeometry_.y() + screenGeometry_.height() - minHeight_ : screenGeometry_.y() + (screenGeometry_.height() - minHeight_) / 2); dockGeometry.setWidth(minWidth_); dockGeometry.setHeight(minHeight_); return dockGeometry; } void DockPanel::updateLayout() { if (isLeaving_) { for (const auto& item : items_) { item->setAnimationStartAsCurrent(); if (isHorizontal()) { startBackgroundWidth_ = backgroundWidth_; startBackgroundHeight_ = minSize_ + 2 * itemSpacing_; } else { // Vertical startBackgroundHeight_ = backgroundHeight_; startBackgroundWidth_ = minSize_ + 2 * itemSpacing_; } } } for (int i = 0; i < itemCount(); ++i) { items_[i]->size_ = minSize_; if (isHorizontal()) { items_[i]->left_ = (i == 0) ? isBottom() && is3D() ? itemSpacing_ + (maxWidth_ - minWidth_) / 2 + margin3D_ : itemSpacing_ + (maxWidth_ - minWidth_) / 2 : items_[i - 1]->left_ + items_[i - 1]->getMinWidth() + itemSpacing_; items_[i]->top_ = isTop() ? itemSpacing_ : itemSpacing_ + maxHeight_ - minHeight_; if (isFloating()) { items_[i]->top_ += floatingMargin_; } items_[i]->minCenter_ = items_[i]->left_ + items_[i]->getMinWidth() / 2; } else { // Vertical items_[i]->left_ = isLeft() ? itemSpacing_ : itemSpacing_ + maxWidth_ - minWidth_; if (isFloating()) { items_[i]->left_ += floatingMargin_; } items_[i]->top_ = (i == 0) ? itemSpacing_ + (maxHeight_ - minHeight_) / 2 : items_[i - 1]->top_ + items_[i - 1]->getMinHeight() + itemSpacing_; items_[i]->minCenter_ = items_[i]->top_ + items_[i]->getMinHeight() / 2; } } backgroundWidth_ = minBackgroundWidth_; backgroundHeight_ = minBackgroundHeight_; if (isLeaving_) { for (const auto& item : items_) { item->endSize_ = item->size_; item->endLeft_ = item->left_; item->endTop_ = item->top_; item->startAnimation(numAnimationSteps_); } endBackgroundWidth_ = minBackgroundWidth_; backgroundWidth_ = startBackgroundWidth_; endBackgroundHeight_ = minBackgroundHeight_; backgroundHeight_ = startBackgroundHeight_; currentAnimationStep_ = 0; isAnimationActive_ = true; animationTimer_->start(32 - model_->zoomingAnimationSpeed()); } else { WindowSystem::setLayer(this, visibility_ == PanelVisibility::AlwaysVisible ? LayerShellQt::Window::LayerBottom : LayerShellQt::Window::LayerTop); isMinimized_ = true; if (autoHide()) { isHidden_ = true; } if (intellihide()) { isHidden_ = intellihideShouldHide(); } update(); // Here we have to wait a bit before setMask() to avoid visual artifacts. QTimer::singleShot(500, [this]{ setMask(); }); } } void DockPanel::updateLayout(int x, int y) { if (isEntering_) { for (const auto& item : items_) { item->startSize_ = item->size_; item->startLeft_ = item->left_; item->startTop_ = item->top_; } startBackgroundWidth_ = minBackgroundWidth_; startBackgroundHeight_ = minBackgroundHeight_; } int first_update_index = -1; int last_update_index = 0; if (isHorizontal()) { items_[0]->left_ = isBottom() && is3D() ? itemSpacing_ + margin3D_ : itemSpacing_; } else { // Vertical items_[0]->top_ = itemSpacing_; } for (int i = 0; i < itemCount(); ++i) { int delta; if (isHorizontal()) { delta = std::abs(items_[i]->minCenter_ - x); } else { // Vertical delta = std::abs(items_[i]->minCenter_ - y); } if (delta < parabolicMaxX_) { if (first_update_index == -1) { first_update_index = i; } last_update_index = i; } items_[i]->size_ = parabolic(delta); if (isHorizontal()) { items_[i]->top_ = isTop() ? itemSpacing_ : itemSpacing_ + tooltipSize_ + maxSize_ - items_[i]->size_; if (isFloating()) { items_[i]->top_ += floatingMargin_; } } else { // Vertical items_[i]->left_ = isLeft() ? itemSpacing_ : itemSpacing_ + tooltipSize_ + maxSize_ - items_[i]->size_; if (isFloating()) { items_[i]->left_ += floatingMargin_; } } if (i > 0) { if (isHorizontal()) { items_[i]->left_ = items_[i - 1]->left_ + items_[i - 1]->getWidth() + itemSpacing_; } else { // Vertical items_[i]->top_ = items_[i - 1]->top_ + items_[i - 1]->getHeight() + itemSpacing_; } } } if (first_update_index == -1) { if ((isHorizontal() && x < maxWidth_ / 2) || (!isHorizontal() && y < maxHeight_ / 2)) { first_update_index = last_update_index = 0; } else { first_update_index = last_update_index = itemCount() - 1; } } for (int i = itemCount() - 1; i >= last_update_index + 1; --i) { if (isHorizontal()) { items_[i]->left_ = (i == itemCount() - 1) ? isBottom() && is3D() ? maxWidth_ - itemSpacing_ - items_[i]->getMinWidth() - margin3D_ : maxWidth_ - itemSpacing_ - items_[i]->getMinWidth() : items_[i + 1]->left_ - items_[i]->getMinWidth() - itemSpacing_; } else { // Vertical items_[i]->top_ = (i == itemCount() - 1) ? maxHeight_ - itemSpacing_ - items_[i]->getMinHeight() : items_[i + 1]->top_ - items_[i]->getMinHeight() - itemSpacing_; } } if (first_update_index == 0 && last_update_index < itemCount() - 1) { for (int i = last_update_index; i >= first_update_index; --i) { if (isHorizontal()) { items_[i]->left_ = items_[i + 1]->left_ - items_[i]->getWidth() - itemSpacing_; } else { // Vertical items_[i]->top_ = items_[i + 1]->top_ - items_[i]->getHeight() - itemSpacing_; } } } if (isEntering_) { for (const auto& item : items_) { item->setAnimationEndAsCurrent(); item->startAnimation(numAnimationSteps_); } if (isHorizontal()) { endBackgroundWidth_ = maxWidth_; backgroundWidth_ = startBackgroundWidth_; endBackgroundHeight_ = minSize_ + 2 * itemSpacing_; backgroundHeight_ = startBackgroundHeight_; } else { // Vertical endBackgroundHeight_ = maxHeight_; backgroundHeight_ = startBackgroundHeight_; endBackgroundWidth_ = minSize_ + 2 * itemSpacing_; backgroundWidth_ = startBackgroundWidth_; } currentAnimationStep_ = 0; isAnimationActive_ = true; isEntering_ = false; animationTimer_->start(32 - model_->zoomingAnimationSpeed()); } mouseX_ = x; mouseY_ = y; //resize(maxWidth_, maxHeight_); WindowSystem::setLayer(this, LayerShellQt::Window::LayerTop); isMinimized_ = false; if (autoHide() || intellihide()) { isHidden_ = false; } setMask(); updateActiveItem(x, y); update(); } void DockPanel::resizeTaskManager() { // Re-calculate panel's size. initLayoutVars(); if (isMinimized_) { updateLayout(); return; } else { // Need to call QWidget::resize(), not DockPanel::resize(), in order not to // mess up the zooming. //QWidget::resize(maxWidth_, maxHeight_); if (isHorizontal()) { backgroundWidth_ = maxWidth_; } else { backgroundHeight_ = maxHeight_; } } const int itemsToKeep = (showApplicationMenu_ ? 1 : 0) + (showPager_ ? WindowSystem::numberOfDesktops() : 0); int left = 0; int top = 0; for (int i = 0; i < itemCount(); ++i) { if (isHorizontal()) { left = (i == 0) ? isBottom() && is3D() ? itemSpacing_ + (maxWidth_ - minWidth_) / 2 + margin3D_ : itemSpacing_ + (maxWidth_ - minWidth_) / 2 : left + items_[i - 1]->getMinWidth() + itemSpacing_; if (i >= itemsToKeep) { items_[i]->minCenter_ = left + items_[i]->getMinWidth() / 2; } } else { // Vertical top = (i == 0) ? itemSpacing_ + (maxHeight_ - minHeight_) / 2 : top + items_[i - 1]->getMinHeight() + itemSpacing_; if (i >= itemsToKeep) { items_[i]->minCenter_ = top + items_[i]->getMinHeight() / 2; } } } int last_update_index = 0; for (int i = itemsToKeep; i < itemCount(); ++i) { int delta; if (isHorizontal()) { delta = std::abs(items_[i]->minCenter_ - mouseX_); } else { // Vertical delta = std::abs(items_[i]->minCenter_ - mouseY_); } if (delta < parabolicMaxX_) { last_update_index = i; } items_[i]->size_ = parabolic(delta); if (isHorizontal()) { items_[i]->top_ = isTop() ? itemSpacing_ : itemSpacing_ + tooltipSize_ + maxSize_ - items_[i]->getHeight(); if (isFloating()) { items_[i]->top_ += floatingMargin_; } } else { // Vertical items_[i]->left_ = isLeft() ? itemSpacing_ : itemSpacing_ + tooltipSize_ + maxSize_ - items_[i]->getWidth(); if (isFloating()) { items_[i]->left_ += floatingMargin_; } } if (i > 0) { if (isHorizontal()) { items_[i]->left_ = items_[i - 1]->left_ + items_[i - 1]->getWidth() + itemSpacing_; } else { // Vertical items_[i]->top_ = items_[i - 1]->top_ + items_[i - 1]->getHeight() + itemSpacing_; } } } for (int i = itemCount() - 1; i >= std::max(itemsToKeep, last_update_index + 1); --i) { if (isHorizontal()) { items_[i]->left_ = (i == itemCount() - 1) ? isBottom() && is3D() ? maxWidth_ - itemSpacing_ - items_[i]->getMinWidth() - margin3D_ : maxWidth_ - itemSpacing_ - items_[i]->getMinWidth() : items_[i + 1]->left_ - items_[i]->getMinWidth() - itemSpacing_; } else { // Vertical items_[i]->top_ = (i == itemCount() - 1) ? maxHeight_ - itemSpacing_ - items_[i]->getMinHeight() : items_[i + 1]->top_ - items_[i]->getMinHeight() - itemSpacing_; } } setMask(); } void DockPanel::setStrut(int width) { LayerShellQt::Window::Anchors anchor = LayerShellQt::Window::AnchorBottom; switch (position_) { case PanelPosition::Top: anchor = LayerShellQt::Window::AnchorTop; break; case PanelPosition::Bottom: anchor = LayerShellQt::Window::AnchorBottom; break; case PanelPosition::Left: anchor = LayerShellQt::Window::AnchorLeft; break; case PanelPosition::Right: anchor = LayerShellQt::Window::AnchorRight; break; } WindowSystem::setAnchorAndStrut(this, anchor, width); } void DockPanel::setMask() { if (isMinimized_) { if (isHorizontal()) { const int x = (maxWidth_ - minWidth_) / 2; const int h = !WindowSystem::hasAutoHideManager() && isHidden_ ? 1 : minHeight_; const int y = isTop() ? 0 : maxHeight_ - h; QWidget::setMask(QRegion(x, y, minWidth_, h)); } else { // Vertical. const int y = (maxHeight_ - minHeight_) / 2; const int w = !WindowSystem::hasAutoHideManager() && isHidden_ ? 1 : minWidth_; const int x = isLeft() ? 0 : maxWidth_ - w; QWidget::setMask(QRegion(x, y, w, minHeight_)); } } else { QWidget::setMask(QRegion(0, 0, maxWidth_, maxHeight_)); } repaint(); } void DockPanel::updatePosition(PanelPosition position) { setPosition(position); reload(); if (isHidden_) { // we have to deactivate, wait then re-activate Auto Hide // otherwise the Auto Hide screen edge's border length would not be updated // correctly. setAutoHide(false); update(); QTimer::singleShot(1000, [this]{ setAutoHide(); }); } saveDockConfig(); } void DockPanel::updateVisibility(PanelVisibility visibility) { setVisibility(visibility); setStrut(); setAutoHide(autoHide() || intellihideShouldHide()); saveDockConfig(); } void DockPanel::setAutoHide(bool on) { if (isHidden_ != on) { isHidden_ = on; } if (!WindowSystem::hasAutoHideManager()) { repaint(); setMask(); return; } Qt::Edge edge = Qt::BottomEdge; switch (position_) { case PanelPosition::Top: edge = Qt::TopEdge; break; case PanelPosition::Bottom: edge = Qt::BottomEdge; break; case PanelPosition::Left: edge = Qt::LeftEdge; break; case PanelPosition::Right: edge = Qt::RightEdge; break; } WindowSystem::setAutoHide(this, edge, on); } void DockPanel::updateActiveItem(int x, int y) { int i = 0; while (i < itemCount() && ((orientation_ == Qt::Horizontal && items_[i]->left_ < x) || (orientation_ == Qt::Vertical && items_[i]->top_ < y))) { ++i; } activeItem_ = i - 1; } int DockPanel::parabolic(int x) { // Assume x >= 0. if (x > parabolicMaxX_) { return minSize_; } else { return maxSize_ - (x * x * (maxSize_ - minSize_)) / (parabolicMaxX_ * parabolicMaxX_); } } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/dock_panel.h000066400000000000000000000353511512233520000217270ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_DOCK_PANEL_H_ #define CRYSTALDOCK_DOCK_PANEL_H_ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "add_panel_dialog.h" #include "application_menu_settings_dialog.h" #include "appearance_settings_dialog.h" #include "dock_item.h" #include "edit_keyboard_layouts_dialog.h" #include "edit_launchers_dialog.h" #include "task_manager_settings_dialog.h" #include "wallpaper_settings_dialog.h" namespace crystaldock { class MultiDockView; // A dock panel. The user can have multiple dock panels at the same time. class DockPanel : public QWidget { Q_OBJECT public: static constexpr int kIconLoadSize = 128; // For certain actions like Lock Screen, we need to delay execution for a bit // to avoid graphical issues. static constexpr int kExecutionDelayMs = 300; static constexpr int k3DPanelThickness = 4; static constexpr int kIndicatorSizeGlass = 10; // for Glass 2D/3D. static constexpr int kIndicatorSizeFlat2D = 6; static constexpr int kIndicatorSizeMetal2D = 8; static constexpr int kIndicatorSpacing = 3; static constexpr int kIndicatorMarginGlass2D = 4; static constexpr float kSpacingMultiplier = 0.5; // for Glass 2D/3D and Flat 2D. static constexpr float kSpacingMultiplierMetal2D = 0.33; static const char kVersion[]; // No pointer ownership. DockPanel(MultiDockView* parent, MultiDockModel* model, int dockId); virtual ~DockPanel() = default; int dockId() const { return dockId_; } PanelPosition position() const { return position_; } QRect screenGeometry() { return screenGeometry_; } void addPanelSettings(QMenu* menu); int itemSpacing() { return itemSpacing_; } bool isHorizontal() { return orientation_ == Qt::Horizontal; } bool isTop() { return position_ == PanelPosition::Top; } bool isBottom() { return position_ == PanelPosition::Bottom; } bool isLeft() { return position_ == PanelPosition::Left; } bool is3D() { return panelStyle_ == PanelStyle::Glass3D_Floating || panelStyle_ == PanelStyle::Glass3D_NonFloating; } bool isGlass2D() { return panelStyle_ == PanelStyle::Glass2D_Floating || panelStyle_ == PanelStyle::Glass2D_NonFloating; } bool isGlass() { return is3D() || isGlass2D(); } bool isFlat2D() { return panelStyle_ == PanelStyle::Flat2D_Floating || panelStyle_ == PanelStyle::Flat2D_NonFloating; } bool isMetal2D() { return panelStyle_ == PanelStyle::Metal2D_Floating || panelStyle_ == PanelStyle::Metal2D_NonFloating; } // position of task indicators, y-coordinate if horizontal, x if vertical. int taskIndicatorPos(); // Gets number of items for an application. Useful when Group Tasks By Application is Off. int itemCount(const QString& appId); // Update pinned status of an application. Useful when Group Tasks By Application is Off. void updatePinnedStatus(const QString& appId, bool pinned); // Sets whether the dock is showing some popup menu. void setShowingPopup(bool showingPopup); public slots: // Reloads the items and updates the dock. void reload(); // Checks that the items are still valid, removes an invalid one and updates the dock. // Should be called after a program with no task is unpinned. // Will return as soon as an invalid one is found. void refresh(); void delayedRefresh(); void onCurrentDesktopChanged(); void onCurrentActivityChanged(); void onDockLaunchersChanged(int dockId) { if (dockId_ == dockId) { reload(); } } void setStrut(); void updatePosition(PanelPosition position); void updateVisibility(PanelVisibility visibility); void setAutoHide(bool on = true); void changeFloatingStyle() { panelStyle_ = static_cast(static_cast(panelStyle_) ^ 1); model_->setPanelStyle(panelStyle_); model_->saveAppearanceConfig(); } void changePanelStyle(PanelStyle style) { model_->setPanelStyle(style); model_->saveAppearanceConfig(); } void toggleApplicationMenu() { showApplicationMenu_ = !showApplicationMenu_; reload(); saveDockConfig(); } void togglePager(); void updatePager() { if (showPager_) { reload(); } } void toggleTaskManager() { model_->setShowTaskManager(dockId_, taskManagerAction_->isChecked()); reload(); saveDockConfig(); } void toggleTrash() { showTrash_ = !showTrash_; reload(); saveDockConfig(); } void toggleWifiManager() { showWifiManager_ = !showWifiManager_; reload(); saveDockConfig(); } void toggleVolumeControl() { showVolumeControl_ = !showVolumeControl_; reload(); saveDockConfig(); } void toggleBatteryIndicator() { showBatteryIndicator_ = !showBatteryIndicator_; reload(); saveDockConfig(); } void toggleKeyboardLayout() { showKeyboardLayout_ = !showKeyboardLayout_; reload(); saveDockConfig(); } void toggleVersionChecker() { showVersionChecker_ = !showVersionChecker_; reload(); saveDockConfig(); } void toggleClock() { showClock_ = !showClock_; reload(); saveDockConfig(); } // Sets the dock on a specific screen given screen index. // Thus 0 is screen 1 and so on. // This doesn't refresh the dock. void setScreen(int screen); // Moves the dock to the new screen. void changeScreen(int screen); // Slot to update zoom animation. void updateAnimation(); void showOnlineDocumentation(); void about(); // These are for global appearance settings. // Dock-specific settings are activated from menu items on the context menu // directly. void showAppearanceSettingsDialog(); void showEditKeyboardLayoutsDialog(); void showEditLaunchersDialog(); void showApplicationMenuSettingsDialog(); void showWallpaperSettingsDialog(int desktop); void showTaskManagerSettingsDialog(); void addDock(); void cloneDock(); void removeDock(); void onWindowAdded(const WindowInfo* info); void onWindowRemoved(void* window); void onWindowLeftCurrentDesktop(void* window); void onWindowLeftCurrentActivity(void* window); void onWindowGeometryChanged(const WindowInfo* task); void onWindowStateChanged(const WindowInfo* info); void onWindowTitleChanged(const WindowInfo* info); void onActiveWindowChanged(); void onWindowEnteredOutput(const WindowInfo*, const wl_output*); void onWindowLeftOutput(const WindowInfo*, const wl_output*); void minimize() { leaveEvent(nullptr); } protected: virtual void paintEvent(QPaintEvent* e) override; virtual void mouseMoveEvent(QMouseEvent* e) override; virtual void mousePressEvent(QMouseEvent* e) override; virtual void wheelEvent(QWheelEvent* e) override; virtual void enterEvent(QEnterEvent* e) override; virtual void leaveEvent(QEvent* e) override; virtual void dragEnterEvent(QDragEnterEvent* e) override; virtual void dragMoveEvent(QDragMoveEvent* e) override; virtual void dropEvent(QDropEvent* e) override; private: // The space between the tooltip and the dock. static constexpr int kTooltipSpacing = 10; bool autoHide() const { return visibility_ == PanelVisibility::AutoHide; } bool intellihide() const { return visibility_ == PanelVisibility::IntelligentAutoHide; } bool isFloating() const { return panelStyle_ == PanelStyle::Glass3D_Floating || panelStyle_ == PanelStyle::Glass2D_Floating || panelStyle_ == PanelStyle::Flat2D_Floating || panelStyle_ == PanelStyle::Metal2D_Floating; } void setPosition(PanelPosition position); void setVisibility(PanelVisibility visibility); void setPanelStyle(PanelStyle panelStyle); int itemCount() const { return static_cast(items_.size()); } int applicationMenuItemCount() const { return showApplicationMenu_ ? 1 : 0; } int launcherItemCount() const { return model_->launcherConfigs(dockId_).size(); } int pagerItemCount() const { return showPager_ ? WindowSystem::numberOfDesktops() : 0; } bool showTaskManager() { return model_->showTaskManager(dockId_); } void initUi(); void createMenu(); void loadDockConfig(); void saveDockConfig(); void loadAppearanceConfig(); void initLaunchers(); void initApplicationMenu(); void initPager(); void initTasks(); void reloadTasks(); // Returns true if it changes the dock layout (i.e. adding a new program icon). bool addTask(const WindowInfo* task); void removeTask(void* window); void updateTask(const WindowInfo* task); bool isValidTask(const WindowInfo* task); bool shouldConsiderTaskForIntellihide(const WindowInfo* task); bool hasTask(void* window); void initTrash(); void initWifiManager(); void initVolumeControl(); void initBatteryIndicator(); void initKeyboardLayout(); void initVersionChecker(); void initClock(); void initLayoutVars(); QRect getMinimizedDockGeometry(); // Updates width, height, items's size and position when the mouse is outside // the dock. void updateLayout(); // Updates width, height, items's size and position given the mouse position. void updateLayout(int x, int y); // Checks if the mouse has actually entered the dock panel's visibility area. bool checkMouseEnter(int x, int y); // Should the dock hide in Intelligent Auto Hide mode? bool intellihideShouldHide(void* excluding_window = nullptr); // Hides/unhides the dock in Intelligent Auto Hide mode if necessary. void intellihideHideUnhide(void* excluding_window = nullptr); // Is the dock empty? // The dock is empty if it has no dock items (separators excluded). bool isEmpty(); // Resizes the task manager part of the panel. This needs to not interfere // with the zooming. void resizeTaskManager(); void setStrut(int width); // Sets the visibility and mouse event region mask appropriately. void setMask(); // Updates the active item given the mouse position. void updateActiveItem(int x, int y); // Drawing logic for each dock style. void drawGlass3D(QPainter& painter); void draw2D(QPainter& painter); // for 2D styles. void drawTooltip(QPainter& painter); // Returns the size given the distance to the mouse. int parabolic(int x); MultiDockView* parent_; // The model. MultiDockModel* model_; int dockId_; // Config variables. PanelPosition position_; int screen_; // the screen (as screen index) that the dock is on. PanelVisibility visibility_; bool showApplicationMenu_; bool showPager_; bool showTrash_; bool showWifiManager_; bool showVolumeControl_; bool showBatteryIndicator_; bool showKeyboardLayout_; bool showVersionChecker_; bool showClock_; int minSize_; int maxSize_; float spacingFactor_; // item spacing as ratio of minSize, in (0, 1) range. QColor backgroundColor_; // including alpha. QColor borderColor_; // no alpha. int tooltipFontSize_; PanelStyle panelStyle_; // Non-config variables. int tooltipSize_; // height (tooltip only shown in horizontal positions). int itemSpacing_; // space between items. int margin3D_; int floatingMargin_; // margin around the dock in floating mode. // Width/height of the dock area when minimized. int minWidth_; int minHeight_; // Width/height of the dock background area (e.g. the background panel in 2D) when minimized. int minBackgroundWidth_; int minBackgroundHeight_; // Width/height of the dock area when maximized (zoomed in). int maxWidth_; int maxHeight_; int parabolicMaxX_; QRect screenGeometry_; // the geometry of the screen that the dock is on. wl_output* screenOutput_; // Number of animation steps when zooming in and out. int numAnimationSteps_; Qt::Orientation orientation_; // The list of all dock items. std::vector> items_; int activeItem_ = -1; // Context (right-click) menu. QMenu menu_; QAction* positionTop_; QAction* positionBottom_; QAction* positionLeft_; QAction* positionRight_; QAction* visibilityAlwaysVisibleAction_; QAction* visibilityIntelligentAutoHideAction_; QAction* visibilityAutoHideAction_; QAction* visibilityAlwaysOnTopAction_; QAction* applicationMenuAction_; QAction* pagerAction_; QAction* taskManagerAction_; QAction* trashAction_; QAction* wifiManagerAction_; QAction* volumeControlAction_; QAction* batteryIndicatorAction_; QAction* keyboardLayoutAction_; QAction* versionCheckerAction_; QAction* clockAction_; QAction* floatingStyleAction_; QAction* glass3DStyleAction_; QAction* glass2DStyleAction_; QAction* flat2DStyleAction_; QAction* metal2DStyleAction_; // Actions to set the dock on a specific screen. std::vector screenActions_; QMessageBox aboutDialog_; AddPanelDialog addPanelDialog_; AppearanceSettingsDialog appearanceSettingsDialog_; EditKeyboardLayoutsDialog editKeyboardLayoutsDialog_; EditLaunchersDialog editLaunchersDialog_; ApplicationMenuSettingsDialog applicationMenuSettingsDialog_; WallpaperSettingsDialog wallpaperSettingsDialog_; TaskManagerSettingsDialog taskManagerSettingsDialog_; bool isMinimized_; // This is needed for Intelligent Auto Hide mode because it could be either be visible // or hidden when minimized. Whereas for Auto Hide mode, it is always hidden when minimized. bool isHidden_; bool isEntering_; bool isLeaving_; bool isAnimationActive_; bool isShowingPopup_; std::unique_ptr animationTimer_; int currentAnimationStep_; int backgroundWidth_; int startBackgroundWidth_; int endBackgroundWidth_; int backgroundHeight_; int startBackgroundHeight_; int endBackgroundHeight_; // For recording the mouse position before doing entering animation // so that we can show the correct tooltip at the end of it. int mouseX_; int mouseY_; friend class KeyboardLayout; // for accessing EditKeyboardLayoutDialog. friend class Program; // for leaveEvent. }; } // namespace crystaldock #endif // CRYSTALDOCK_DOCK_PANEL_H_ dangvd-crystal-dock-7a34e96/src/view/edit_keyboard_layouts_dialog.cc000066400000000000000000000065141512233520000256710ustar00rootroot00000000000000#include "edit_keyboard_layouts_dialog.h" #include "ui_edit_keyboard_layouts_dialog.h" #include "keyboard_layout.h" #include namespace crystaldock { EditKeyboardLayoutsDialog::EditKeyboardLayoutsDialog(QWidget *parent, MultiDockModel* model) : QDialog(parent), ui(new Ui::EditKeyboardLayoutsDialog), model_(model) { ui->setupUi(this); connect(ui->languages, &QListWidget::currentTextChanged, this, &EditKeyboardLayoutsDialog::onLanguageChanged); connect(ui->addButton, &QPushButton::pressed, this, &EditKeyboardLayoutsDialog::onAddButtonClicked); connect(ui->removeButton, &QPushButton::pressed, this, &EditKeyboardLayoutsDialog::onRemoveButtonClicked); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &EditKeyboardLayoutsDialog::saveData); } EditKeyboardLayoutsDialog::~EditKeyboardLayoutsDialog() { delete ui; } void EditKeyboardLayoutsDialog::setKeyboardLayouts( const std::map>& keyboardLayouts, const std::map& keyboardEngines) { keyboardLayouts_ = keyboardLayouts; keyboardEngines_ = keyboardEngines; ui->languages->clear(); for (const auto& pair : keyboardLayouts_) { ui->languages->addItem(pair.first); } } void EditKeyboardLayoutsDialog::refreshData() { ui->userKeyboardLayouts->clear(); for (const auto& layout : model_->userKeyboardLayouts()) { if (keyboardEngines_.count(layout) > 0) { auto* item = new QListWidgetItem; item->setText(keyboardEngines_[layout].toString()); item->setData(Qt::UserRole, QVariant::fromValue(keyboardEngines_[layout])); ui->userKeyboardLayouts->addItem(item); } } } void EditKeyboardLayoutsDialog::onLanguageChanged(const QString& language) { if (keyboardLayouts_.count(language) == 0) { return; } ui->languageKeyboardLayouts->clear(); for (const auto& layout : keyboardLayouts_[language]) { auto* item = new QListWidgetItem; item->setText(layout.toString()); item->setData(Qt::UserRole, QVariant::fromValue(layout)); ui->languageKeyboardLayouts->addItem(item); } } void EditKeyboardLayoutsDialog::onAddButtonClicked() { auto* item = ui->languageKeyboardLayouts->currentItem(); if (item == nullptr) { return; } auto info = item->data(Qt::UserRole).value(); if (ui->userKeyboardLayouts->findItems(info.toString(), Qt::MatchExactly).isEmpty()) { auto* newItem = new QListWidgetItem; newItem->setText(info.toString()); newItem->setData(Qt::UserRole, QVariant::fromValue(info)); ui->userKeyboardLayouts->addItem(newItem); } } void EditKeyboardLayoutsDialog::onRemoveButtonClicked() { auto* item = ui->userKeyboardLayouts->currentItem(); if (item == nullptr) { return; } delete item; } void EditKeyboardLayoutsDialog::saveData() { QStringList userLayouts; for (int i = 0; i < ui->userKeyboardLayouts->count(); ++i) { auto* item = ui->userKeyboardLayouts->item(i); auto info = item->data(Qt::UserRole).value(); userLayouts << info.engine; } model_->setUserKeyboardLayouts(userLayouts); if (!userLayouts.contains(model_->activeKeyboardLayout()) && !userLayouts.isEmpty()) { model_->setActiveKeyboardLayout(userLayouts[0]); } model_->saveAppearanceConfig(); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/edit_keyboard_layouts_dialog.h000066400000000000000000000025451512233520000255330ustar00rootroot00000000000000#ifndef CRYSTAL_DOCK_EDIT_KEYBOARD_LAYOUTS_DIALOG_H_ #define CRYSTAL_DOCK_EDIT_KEYBOARD_LAYOUTS_DIALOG_H_ #include #include #include namespace Ui { class EditKeyboardLayoutsDialog; } namespace crystaldock { class MultiDockModel; struct KeyboardLayoutInfo; class EditKeyboardLayoutsDialog : public QDialog { Q_OBJECT public: explicit EditKeyboardLayoutsDialog(QWidget* parent, MultiDockModel* model); ~EditKeyboardLayoutsDialog(); void setKeyboardLayouts( const std::map>& keyboardLayouts, const std::map& keyboardEngines); void refreshData(); public slots: void onLanguageChanged(const QString&); void onAddButtonClicked(); void onRemoveButtonClicked(); void saveData(); private: Ui::EditKeyboardLayoutsDialog *ui; MultiDockModel* model_; // TODO: It's not great to duplicate this here (they are also in KeyboardLayout). // Consider moving them to the model. // All the available keyboard layouts, as map from languages to list of structs. std::map> keyboardLayouts_; // All the available keyboard layouts, as map from engines to structs. std::map keyboardEngines_; }; } // namespace crystaldock #endif // CRYSTAL_DOCK_EDIT_KEYBOARD_LAYOUTS_DIALOG_H_ dangvd-crystal-dock-7a34e96/src/view/edit_keyboard_layouts_dialog.ui000066400000000000000000000074671512233520000257310ustar00rootroot00000000000000 EditKeyboardLayoutsDialog 0 0 950 600 Edit Keyboard Layouts 220 540 451 32 Qt::Orientation::Horizontal QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok true 30 60 371 441 590 60 321 160 590 280 321 220 440 330 120 38 Add 440 400 120 38 Remove 590 20 200 32 Languages 590 240 300 32 Available Keyboard Layouts 30 20 300 32 Keyboard Layouts buttonBox accepted() EditKeyboardLayoutsDialog accept() 248 254 157 274 buttonBox rejected() EditKeyboardLayoutsDialog reject() 316 260 286 274 dangvd-crystal-dock-7a34e96/src/view/edit_launchers_dialog.cc000066400000000000000000000152131512233520000242710ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "edit_launchers_dialog.h" #include "ui_edit_launchers_dialog.h" #include #include #include #include #include #include #include #include namespace crystaldock { QDataStream &operator<<(QDataStream &out, const LauncherInfo& launcher) { out << launcher.iconName << launcher.appId; return out; } QDataStream &operator>>(QDataStream &in, LauncherInfo& launcher) { in >> launcher.iconName >> launcher.appId; return in; } LauncherList::LauncherList(EditLaunchersDialog* parent) : QListWidget(parent), parent_(parent) {} void LauncherList::dragEnterEvent(QDragEnterEvent *event) { // Internal drag-and-drop. LauncherList* source = dynamic_cast(event->source()); if (source != nullptr && source == this) { event->acceptProposedAction(); setDragDropMode(QAbstractItemView::InternalMove); return; } // External drag-and-drop. if (event->mimeData()->hasFormat("text/uri-list")) { QString fileUrl = QString(event->mimeData()->data("text/uri-list")).trimmed(); if (fileUrl.endsWith(".desktop")) { event->acceptProposedAction(); setDragDropMode(QAbstractItemView::DragDrop); } } } void LauncherList::dragMoveEvent(QDragMoveEvent* event) { event->acceptProposedAction(); } void LauncherList::dropEvent(QDropEvent* event) { // External drag-and-drop. if (event->mimeData()->hasFormat("text/uri-list")) { QString fileUrl = QString(event->mimeData()->data("text/uri-list")).trimmed(); DesktopFile desktopFile(QUrl(fileUrl).toLocalFile()); parent_->addLauncher(desktopFile.name(), desktopFile.appId(), desktopFile.icon()); } else { // Internal drag-and-drop. QListWidget::dropEvent(event); } } EditLaunchersDialog::EditLaunchersDialog(QWidget* parent, MultiDockModel* model, int dockId) : QDialog(parent), ui(new Ui::EditLaunchersDialog), model_(model), dockId_(dockId) { ui->setupUi(this); launchers_ = new LauncherList(this); launchers_->setGeometry(QRect(20, 20, 350, 490)); launchers_->setSelectionMode(QAbstractItemView::SingleSelection); launchers_->setDragEnabled(true); launchers_->setAcceptDrops(true); launchers_->setDropIndicatorShown(true); launchers_->setDragDropMode(QAbstractItemView::DragDrop); setWindowFlag(Qt::Tool); qRegisterMetaType(); connect(ui->systemCommands, SIGNAL(currentIndexChanged(int)), this, SLOT(addSystemCommand(int))); connect(ui->addSeparator, SIGNAL(clicked()), this, SLOT(addSeparator())); connect(ui->addLauncherSeparator, SIGNAL(clicked()), this, SLOT(addLauncherSeparator())); connect(ui->remove, SIGNAL(clicked()), this, SLOT(removeSelectedLauncher())); connect(ui->removeAll, SIGNAL(clicked()), this, SLOT(removeAllLaunchers())); connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonClicked(QAbstractButton*))); initSystemCommands(); loadData(); } void EditLaunchersDialog::addLauncher(const QString& name, const QString& appId, const QString& iconName) { QListWidgetItem* listItem; if (appId == kSeparatorId) { listItem = new QListWidgetItem("--- Separator ---"); } else if (appId == kLauncherSeparatorId) { listItem = new QListWidgetItem("--- Launcher Separator ---"); } else if (appId == kShowDesktopId) { listItem = new QListWidgetItem( QIcon::fromTheme(kShowDesktopIcon).pixmap(kListIconSize), kShowDesktopName); } else { listItem = new QListWidgetItem( QIcon::fromTheme(iconName).pixmap(kListIconSize), name); } listItem->setData(Qt::UserRole, QVariant::fromValue(LauncherInfo(iconName, appId))); launchers_->addItem(listItem); launchers_->setCurrentItem(listItem); } void EditLaunchersDialog::accept() { QDialog::accept(); saveData(); } void EditLaunchersDialog::buttonClicked(QAbstractButton* button) { auto role = ui->buttonBox->buttonRole(button); if (role == QDialogButtonBox::ApplyRole) { saveData(); } } void EditLaunchersDialog::addSystemCommand(int index) { if (index <= 0) { // Excludes header item. return; } LauncherInfo info = ui->systemCommands->currentData().value(); addLauncher(ui->systemCommands->currentText(), info.appId, info.iconName); } void EditLaunchersDialog::addSeparator() { addLauncher("Separator", kSeparatorId, /*iconName=*/""); } void EditLaunchersDialog::addLauncherSeparator() { addLauncher("Launcher Separator", kLauncherSeparatorId, /*iconName=*/""); } void EditLaunchersDialog::removeSelectedLauncher() { QListWidgetItem* item = launchers_->takeItem(launchers_->currentRow()); if (item != nullptr) { delete item; } } void EditLaunchersDialog::removeAllLaunchers() { launchers_->clear(); } void EditLaunchersDialog::initSystemCommands() { ui->systemCommands->addItem(getListItemIcon(kShowDesktopIcon), kShowDesktopName, QVariant::fromValue(LauncherInfo(kShowDesktopIcon, kShowDesktopId))); for (const auto& category : model_->applicationMenuSystemCategories()) { for (const auto& entry : category.entries) { ui->systemCommands->addItem( getListItemIcon(entry.icon), entry.name, QVariant::fromValue(LauncherInfo(entry.icon, entry.appId))); } } } void EditLaunchersDialog::loadData() { launchers_->clear(); for (const auto& item : model_->launcherConfigs(dockId_)) { addLauncher(item.name, item.appId, item.icon); } launchers_->setCurrentRow(0); ui->systemCommands->setCurrentIndex(0); } void EditLaunchersDialog::saveData() { const int launcherCount = launchers_->count(); QStringList launchers; for (int i = 0; i < launcherCount; ++i) { auto* listItem = launchers_->item(i); auto info = listItem->data(Qt::UserRole).value(); launchers.append(info.appId); } model_->setLaunchers(dockId_, launchers); model_->saveDockConfig(dockId_); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/edit_launchers_dialog.h000066400000000000000000000057241512233520000241410ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_EDIT_LAUNCHERS_DIALOG_H_ #define CRYSTALDOCK_EDIT_LAUNCHERS_DIALOG_H_ #include #include #include #include #include #include #include #include #include namespace Ui { class EditLaunchersDialog; } namespace crystaldock { // User data for the items in QListWidget/QComboBox. struct LauncherInfo { // The name(label) is already stored as item text in QListWidget/QComboBox. QString iconName; QString appId; LauncherInfo() {} LauncherInfo(QString iconName2, QString appId2) : iconName(iconName2), appId(appId2) {} }; QDataStream &operator<<(QDataStream &out, const LauncherInfo& launcher); QDataStream &operator>>(QDataStream &in, LauncherInfo& launcher); class EditLaunchersDialog; class LauncherList : public QListWidget { public: explicit LauncherList(EditLaunchersDialog* parent); protected: void dragEnterEvent(QDragEnterEvent *event) override; void dragMoveEvent(QDragMoveEvent* event) override; void dropEvent(QDropEvent *event) override; private: EditLaunchersDialog* parent_; }; class EditLaunchersDialog : public QDialog { Q_OBJECT public: EditLaunchersDialog(QWidget* parent, MultiDockModel* model, int dockId); ~EditLaunchersDialog() = default; void reload() { loadData(); } void addLauncher(const QString& name, const QString& appId, const QString& iconName); public slots: void accept() override; void buttonClicked(QAbstractButton* button); void addSystemCommand(int index); void addSeparator(); void addLauncherSeparator(); void removeSelectedLauncher(); void removeAllLaunchers(); private: static constexpr int kListIconSize = 48; void initSystemCommands(); void loadData(); void saveData(); QIcon getListItemIcon(const QString& iconName) { return QIcon::fromTheme(iconName).pixmap(kListIconSize); } void populateSystemCommands(); Ui::EditLaunchersDialog* ui; LauncherList* launchers_; MultiDockModel* model_; int dockId_; friend class EditLaunchersDialogTest; }; } // namespace crystaldock Q_DECLARE_METATYPE(crystaldock::LauncherInfo); #endif // CRYSTALDOCK_EDIT_LAUNCHERS_DIALOG_H_ dangvd-crystal-dock-7a34e96/src/view/edit_launchers_dialog.ui000066400000000000000000000121321512233520000243160ustar00rootroot00000000000000 EditLaunchersDialog 0 0 820 790 Edit Launchers 40 720 741 35 Qt::Orientation::Horizontal QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok true 20 520 771 180 <html><head/><body><p>Tips: </p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">To add a standard program launcher, either use drag-and-drop from any application list to the launcher list in this dialog, or right-click a running program on the dock panel then select &quot;Pinned&quot;.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Use drag-and-drop within the launcher list to re-order the items.</li></ul></body></html> Qt::TextFormat::RichText true 420 325 361 40 Remove Selected 420 405 361 40 Remove All 420 165 361 40 <html><head/><body><p>A regular separator. Task icons for unpinned applications will be placed before this separator.</p></body></html> Add Separator 420 85 361 44 true Add System Command 420 245 361 40 <html><head/><body><p>A launcher separator. Task icons for unpinned applications will be placed after this separator.</p></body></html> Add Launcher Separator buttonBox accepted() EditLaunchersDialog accept() 248 254 157 274 buttonBox rejected() EditLaunchersDialog reject() 316 260 286 274 dangvd-crystal-dock-7a34e96/src/view/icon_based_dock_item.cc000066400000000000000000000063611512233520000240710ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "icon_based_dock_item.h" #include #include #include "dock_panel.h" #include #include namespace crystaldock { IconBasedDockItem::IconBasedDockItem(DockPanel* parent, MultiDockModel* model, const QString& label, Qt::Orientation orientation, const QString& iconName, int minSize, int maxSize) : DockItem(parent, model, label, orientation, minSize, maxSize), icons_(maxSize - minSize + 1) { setIconName(iconName); } IconBasedDockItem::IconBasedDockItem(DockPanel* parent, MultiDockModel* model, const QString& label, Qt::Orientation orientation, const QPixmap& icon, int minSize, int maxSize) : DockItem(parent, model, label, orientation, minSize, maxSize), icons_(maxSize - minSize + 1) { setIcon(icon); } void IconBasedDockItem::draw(QPainter* painter) const { const auto& icon = icons_[size_ - minSize_]; if (!icon.isNull()) { painter->drawPixmap(left_, top_, icon); } else { // Fall-back "icon". QColor fillColor = model_->backgroundColor(); fillColor.setAlphaF(kDefaultBackgroundAlpha); drawFallbackIcon(left_, top_, size_, model_->borderColor(), fillColor, painter); } } void IconBasedDockItem::setIcon(const QPixmap& icon) { generateIcons(icon); } void IconBasedDockItem::setIconName(const QString& iconName, const QString& backupIconName) { QPixmap icon = loadIcon(iconName, DockPanel::kIconLoadSize); if (icon.isNull() && !backupIconName.isEmpty()) { icon = loadIcon(backupIconName, DockPanel::kIconLoadSize); } if (!icon.isNull()) { iconName_ = iconName; setIcon(icon); } } const QPixmap& IconBasedDockItem::getIcon(int size) const { if (size < minSize_) { size = minSize_; } else if (size > maxSize_) { size = maxSize_; } return icons_[size - minSize_]; } void IconBasedDockItem::generateIcons(const QPixmap& icon) { QImage image = icon.toImage(); // Convert to QImage for fast scaling. if (image.isNull()) { return; } for (int size = minSize_; size <= maxSize_; ++size) { icons_[size - minSize_] = QPixmap::fromImage( (orientation_ == Qt::Horizontal) ? image.scaledToHeight(size, Qt::SmoothTransformation) : image.scaledToWidth(size, Qt::SmoothTransformation)); //https://doc.qt.io/qt-6/highdpi.html icons_[size - minSize_].setDevicePixelRatio(1.0f); } } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/icon_based_dock_item.h000066400000000000000000000044041512233520000237270ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_ICON_BASED_DOCK_ITEM_H_ #define CRYSTALDOCK_ICON_BASED_DOCK_ITEM_H_ #include #include #include #include #include #include "dock_item.h" namespace crystaldock { // Base class for icon-based dock items, such as launchers and pager icons. class IconBasedDockItem : public DockItem { public: IconBasedDockItem(DockPanel* parent, MultiDockModel* model, const QString& label, Qt::Orientation orientation, const QString& iconName, int minSize, int maxSize); IconBasedDockItem(DockPanel* parent, MultiDockModel* model, const QString& label, Qt::Orientation orientation, const QPixmap& icon, int minSize, int maxSize); virtual ~IconBasedDockItem() {} int getWidthForSize(int size) const override { const auto& icon = getIcon(size); return !icon.isNull() ? icon.width() : size; } int getHeightForSize(int size) const override { const auto& icon = getIcon(size); return !icon.isNull() ? icon.height() : size; } void draw(QPainter* painter) const override; // Sets the icon on the fly. void setIcon(const QPixmap& icon); void setIconName(const QString& iconName, const QString& backupIconName = ""); const QPixmap& getIcon(int size) const; QString getIconName() const { return iconName_; } protected: std::vector icons_; QString iconName_; private: void generateIcons(const QPixmap& icon); friend class DockPanel; }; } // namespace crystaldock #endif // CRYSTALDOCK_ICON_BASED_DOCK_ITEM_H_ dangvd-crystal-dock-7a34e96/src/view/icon_button.cc000066400000000000000000000017451512233520000223110ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "icon_button.h" #include namespace crystaldock { void IconButton::setIcon(const QString& icon) { icon_ = icon; QPushButton::setIcon(QIcon::fromTheme(icon)); setIconSize(size() - QSize(10, 10)); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/icon_button.h000066400000000000000000000022341512233520000221450ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_ICON_BUTTON_H_ #define CRYSTALDOCK_ICON_BUTTON_H_ #include #include namespace crystaldock { class IconButton : public QPushButton { Q_OBJECT public: IconButton(QWidget *parent = nullptr) : QPushButton(parent) {} QString icon() { return icon_; } void setIcon(const QString& icon); private: QString icon_; }; } // namespace crystaldock #endif // CRYSTALDOCK_ICON_BUTTON_H_ dangvd-crystal-dock-7a34e96/src/view/iconless_dock_item.cc000066400000000000000000000021631512233520000236160ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "iconless_dock_item.h" namespace crystaldock { int IconlessDockItem::getWidthForSize(int size) const { return isHorizontal() ? static_cast(size * whRatio_) : size; } int IconlessDockItem::getHeightForSize(int size) const { return isHorizontal() ? size : static_cast(reverseWhRatio_ ? (size * whRatio_) : (size / whRatio_)); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/iconless_dock_item.h000066400000000000000000000032261512233520000234610ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_ICONLESS_DOCK_ITEM_H_ #define CRYSTALDOCK_ICONLESS_DOCK_ITEM_H_ #include "dock_item.h" namespace crystaldock { // Base class for dock items without an icon, such as clock. class IconlessDockItem : public DockItem { public: IconlessDockItem(DockPanel* parent, MultiDockModel* model, const QString& label, Qt::Orientation orientation, int minSize, int maxSize, float whRatio, bool reverseWhRatio = false) : DockItem(parent, model, label, orientation, minSize, maxSize), whRatio_(whRatio), reverseWhRatio_(reverseWhRatio) {} virtual ~IconlessDockItem() {} int getWidthForSize(int size) const override; int getHeightForSize(int size) const override; protected: // Width/height ratio. float whRatio_; // Iff true, reverse width/height ratio when the orientation is vertical. bool reverseWhRatio_; }; } // namespace crystaldock #endif // CRYSTALDOCK_ICONLESS_DOCK_ITEM_H_ dangvd-crystal-dock-7a34e96/src/view/keyboard_layout.cc000066400000000000000000000204101512233520000231510ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "keyboard_layout.h" #include #include #include #include #include "dock_panel.h" #include #include namespace crystaldock { KeyboardLayout::KeyboardLayout(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize) : IconBasedDockItem(parent, model, kLabel, orientation, kIcon, minSize, maxSize), process_(nullptr) { initKeyboardLayouts(); connect(&menu_, &QMenu::triggered, this, &KeyboardLayout::onKeyboardLayoutSelected); connect(&menu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); connect(&contextMenu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); } KeyboardLayout::~KeyboardLayout() { if (process_ && process_->state() != QProcess::NotRunning) { process_->kill(); process_->waitForFinished(1000); } } void KeyboardLayout::draw(QPainter* painter) const { IconBasedDockItem::draw(painter); QFont font; font.setPixelSize(getHeight() / 2); painter->setFont(font); drawBorderedText(left_ + getWidth() / 4, top_ + getHeight() * 3 / 8, getWidth() * 3 / 4, getHeight() * 5 / 8, 0, activeKeyboardLayout_.languageCode, 2, Qt::black, Qt::white, painter); } void KeyboardLayout::mousePressEvent(QMouseEvent* e) { if (e->button() == Qt::LeftButton) { if (commandExists({kCommand}).isEmpty()) { QMessageBox::warning(parent_, "Command not found", QString("Command '") + kCommand + "' not found. This is required by the " + kLabel + " component."); return; } else if (!ibusReady_) { QMessageBox::warning(parent_, "IBus is not running", "Please make sure the IBus daemon is running."); return; } showPopupMenu(&menu_); } else if (e->button() == Qt::RightButton) { showPopupMenu(&contextMenu_); } } QString KeyboardLayout::getLabel() const { return activeKeyboardLayout_.isEmpty() ? kLabel : QString(kLabel) + ": " + activeKeyboardLayout_.toString(); } void KeyboardLayout::onKeyboardLayoutSelected(QAction* action) { KeyboardLayoutInfo layout = action->data().value(); setKeyboardLayout(layout); } void KeyboardLayout::setKeyboardLayout(const KeyboardLayoutInfo& layout) { // Prevent concurrent processes if (process_ && process_->state() != QProcess::NotRunning) { return; } process_ = new QProcess(parent_); connect(process_, QOverload::of(&QProcess::finished), [this, layout](int exitCode, QProcess::ExitStatus exitStatus) { // Somehow IBus returns 1 here even when it succeeded. activeKeyboardLayout_ = layout; model_->setActiveKeyboardLayout(activeKeyboardLayout_.engine); model_->saveAppearanceConfig(/*repaintOnly=*/true); process_->deleteLater(); process_ = nullptr; }); process_->start(kCommand, QStringList() << "engine" << layout.engine); } void KeyboardLayout::initKeyboardLayouts() { if (process_ && process_->state() != QProcess::NotRunning) { return; } process_ = new QProcess(parent_); connect(process_, QOverload::of(&QProcess::finished), [this](int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode == 0) { ibusReady_ = true; QRegularExpression languageRe(R"(language:\s+(.+))"); QRegularExpression keyboardRe(R"(\s*(.+)\s+-\s+(.+))"); QString language; for (const QByteArray& line : process_->readAllStandardOutput().split('\n')) { QRegularExpressionMatch match = languageRe.match(line); if (match.hasMatch()) { language = match.captured(1).trimmed(); } else { match = keyboardRe.match(line); if (match.hasMatch()) { QString engine = match.captured(1).trimmed(); QString description = match.captured(2).trimmed(); if (!language.isEmpty()) { if (keyboardLayouts_.count(language) == 0) { keyboardLayouts_[language] = std::vector(); } keyboardLayouts_[language].push_back( KeyboardLayoutInfo(language, engine, description)); keyboardEngines_[engine] = keyboardLayouts_[language].back(); } } } } parent_->editKeyboardLayoutsDialog_.setKeyboardLayouts( keyboardLayouts_, keyboardEngines_); QString activeLayout = model_->activeKeyboardLayout(); if (!activeLayout.isEmpty() && keyboardEngines_.count(activeLayout) > 0) { initUserKeyboardLayouts(activeLayout); } else { // Gets the currently active keyboard layout. QProcess* process = new QProcess(parent_); connect(process, QOverload::of(&QProcess::finished), [this, process](int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode == 0) { const QString ibusActiveLayout = process->readAllStandardOutput().trimmed(); if (!ibusActiveLayout.isEmpty() && keyboardEngines_.count(ibusActiveLayout) > 0) { initUserKeyboardLayouts(ibusActiveLayout); model_->setActiveKeyboardLayout(ibusActiveLayout); } } process->deleteLater(); }); process->start(kCommand, QStringList() << "engine"); } } process_->deleteLater(); process_ = nullptr; }); process_->start(kCommand, QStringList() << "list-engine"); } void KeyboardLayout::initUserKeyboardLayouts(const QString& activeLayout) { activeKeyboardLayout_ = keyboardEngines_[activeLayout]; QStringList userLayouts = model_->userKeyboardLayouts(); if (userLayouts.isEmpty()) { model_->setUserKeyboardLayouts(QStringList() << activeLayout); } if (!userLayouts.contains(activeLayout)) { userLayouts << activeLayout; } for (const auto& layout : userLayouts) { if (keyboardEngines_.count(layout) > 0) { userKeyboardLayouts_.push_back(keyboardEngines_[layout]); } } createMenu(); QTimer::singleShot(500, this, [this] { setKeyboardLayout(activeKeyboardLayout_); }); } void KeyboardLayout::createMenu() { // Left-click menu. for (const auto& layout : userKeyboardLayouts_) { QAction* action = new QAction(layout.toString(), &menu_); action->setData(QVariant::fromValue(layout)); menu_.addAction(action); } // Right-click context menu. contextMenu_.addSection(kLabel); contextMenu_.addAction(QIcon::fromTheme("configure"), QString("&Edit Keyboard Layouts"), parent_, [this] { parent_->minimize(); QTimer::singleShot(DockPanel::kExecutionDelayMs, [this]{ parent_->showEditKeyboardLayoutsDialog(); }); }); contextMenu_.addSeparator(); parent_->addPanelSettings(&contextMenu_); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/keyboard_layout.h000066400000000000000000000064301512233520000230210ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTAL_DOCK_KEYBOARD_LAYOUT_H_ #define CRYSTAL_DOCK_KEYBOARD_LAYOUT_H_ #include "icon_based_dock_item.h" #include #include #include #include #include #include #include namespace crystaldock { struct KeyboardLayoutInfo { QString language; QString languageCode; QString engine; QString description; KeyboardLayoutInfo() = default; KeyboardLayoutInfo(const QString& language2, const QString& engine2, const QString& description2) : language(language2), engine(engine2), description(description2) { if (language.size() >= 2) { languageCode = language.first(2).toUpper(); } } bool isEmpty() const { return engine.isEmpty(); } QString toString() const { return language + " - " + description; } }; // A keyboard layout manager that integrates with IBus. class KeyboardLayout : public QObject, public IconBasedDockItem { Q_OBJECT public: KeyboardLayout(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize); virtual ~KeyboardLayout(); void draw(QPainter* painter) const override; void mousePressEvent(QMouseEvent* e) override; QString getLabel() const override; bool beforeTask(const QString& program) override { return false; } public slots: void onKeyboardLayoutSelected(QAction* action); void setKeyboardLayout(const KeyboardLayoutInfo& layout); private: static constexpr char kCommand[] = "ibus"; static constexpr char kLabel[] = "Keyboard Layout"; static constexpr char kIcon[] = "input-keyboard"; static constexpr int kUpdateInterval = 1000; // 1 second. void initKeyboardLayouts(); void initUserKeyboardLayouts(const QString& activeLayout); // Creates the context menu. void createMenu(); // All the available keyboard layouts, as map from languages to list of structs. std::map> keyboardLayouts_; // All the available keyboard layouts, as map from engines to structs. std::map keyboardEngines_; // The user-selected keyboard layouts for quick switching. std::vector userKeyboardLayouts_; // The active keyboard layout. KeyboardLayoutInfo activeKeyboardLayout_; bool ibusReady_ = false; // ibus process. QProcess* process_ = nullptr; // Left-click volume menu. QMenu menu_; // Right-click context menu. QMenu contextMenu_; }; } // namespace crystaldock Q_DECLARE_METATYPE(crystaldock::KeyboardLayoutInfo); #endif // CRYSTAL_DOCK_KEYBOARD_LAYOUT_H_ dangvd-crystal-dock-7a34e96/src/view/multi_dock_view.cc000066400000000000000000000067411512233520000231530ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "multi_dock_view.h" #include #include #include #include "display/window_system.h" #include "add_panel_dialog.h" namespace crystaldock { MultiDockView::MultiDockView(MultiDockModel* model) : model_(model), desktopEnv_(DesktopEnv::getDesktopEnv()) { connect(model_, SIGNAL(dockAdded(int)), this, SLOT(onDockAdded(int))); connect(model_, SIGNAL(wallpaperChanged(int)), this, SLOT(setWallpaper(int))); connect(WindowSystem::self(), SIGNAL(currentDesktopChanged(std::string_view)), this, SLOT(setWallpaper())); loadData(); } /* static */ bool MultiDockView::checkPlatformSupported(const QApplication& app) { if (QGuiApplication::platformName().toLower() != "wayland") { QMessageBox::critical(nullptr, "Unsupported Platform", "Crystal Dock 2.x only supports Wayland.\n" "For X11, please use Crystal Dock 1.x"); return false; } QNativeInterface::QWaylandApplication* waylandApp = app.nativeInterface(); if (!waylandApp) { return false; } return WindowSystem::init(waylandApp->display()); } void MultiDockView::show() { for (const auto& dock : docks_) { dock.second->show(); } setWallpaper(); } void MultiDockView::exit() { for (const auto& dock : docks_) { dock.second->close(); } } void MultiDockView::onDockAdded(int dockId) { docks_[dockId] = std::make_unique(this, model_, dockId); docks_[dockId]->show(); } bool MultiDockView::setWallpaper() { if (!model_->hasPager()) { return false; } for (unsigned int screen = 0; screen < WindowSystem::screens().size(); ++screen) { if (!setWallpaper(screen)) { return false; } } return true; } bool MultiDockView::setWallpaper(int screen) { if (!model_->hasPager()) { return false; } QString wallpaper = model_->wallpaper(WindowSystem::currentDesktop(), screen); if (wallpaper.isEmpty()) { return false; // nothing to do here. } if (!QFile::exists(wallpaper)) { QMessageBox warning(QMessageBox::Warning, "Error", QString("Failed to load wallpaper from: ") + wallpaper, QMessageBox::Ok, nullptr, Qt::Tool); warning.exec(); return false; } return desktopEnv_->setWallpaper(screen, wallpaper); } void MultiDockView::loadData() { docks_.clear(); for (int dockId = 1; dockId <= model_->dockCount(); ++dockId) { docks_[dockId] = std::make_unique(this, model_, dockId); } if (docks_.empty()) { AddPanelDialog dialog(nullptr, model_, 0 /* dockId not needed */); dialog.setMode(AddPanelDialog::Mode::Welcome); dialog.exec(); } } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/multi_dock_view.h000066400000000000000000000032731512233520000230120ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_MULTI_DOCK_VIEW_H_ #define CRYSTALDOCK_MULTI_DOCK_VIEW_H_ #include #include #include #include #include "dock_panel.h" #include #include namespace crystaldock { // The view. class MultiDockView : public QObject { Q_OBJECT public: // No pointer ownership. explicit MultiDockView(MultiDockModel* model); ~MultiDockView() = default; static bool checkPlatformSupported(const QApplication& app); void show(); public slots: void exit(); void onDockAdded(int dockId); bool setWallpaper(); bool setWallpaper(int screen); private: void loadData(); // Creates a default dock if none exists. void createDefaultDock(); MultiDockModel* model_; // No ownership. std::unordered_map> docks_; DesktopEnv* desktopEnv_; }; } // namespace crystaldock #endif // CRYSTALDOCK_MULTI_DOCK_VIEW_H_ dangvd-crystal-dock-7a34e96/src/view/program.cc000066400000000000000000000354361512233520000214410ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "program.h" #include #include #include #include #include #include #include #include #include "display/window_system.h" #include "dock_panel.h" #include namespace crystaldock { Program::Program(DockPanel* parent, MultiDockModel* model, const QString& appId, const QString& label, Qt::Orientation orientation, const QPixmap& icon, int minSize, int maxSize, const QString& command, bool isAppMenuEntry, bool pinned) : IconBasedDockItem(parent, model, label, orientation, icon, minSize, maxSize), appId_(appId), appLabel_(label), command_(command), isAppMenuEntry_(isAppMenuEntry), pinned_(pinned), demandsAttention_(false), attentionStrong_(false), launching_(false) { init(); } Program::Program(DockPanel* parent, MultiDockModel* model, const QString& appId, const QString& label, Qt::Orientation orientation, const QPixmap& icon, int minSize, int maxSize) : IconBasedDockItem(parent, model, label, orientation, icon, minSize, maxSize), appId_(appId), appLabel_(label), command_(""), isAppMenuEntry_(false), pinned_(false), demandsAttention_(false), attentionStrong_(false), launching_(false) { init(); } void Program::init() { createMenu(); animationTimer_.setInterval(500); connect(&animationTimer_, &QTimer::timeout, this, [this]() { attentionStrong_ = !attentionStrong_; parent_->update(); }); bounceTimer_.setInterval(kBounceIntervalMs); connect(&bounceTimer_, &QTimer::timeout, this, &Program::updateBounceAnimation); connect(&menu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); } void Program::draw(QPainter *painter) const { painter->save(); painter->setRenderHint(QPainter::Antialiasing); auto taskCount = static_cast(tasks_.size()); // For launching feedback if bouncing launcher icon is not enabled. if (taskCount == 0 && launching_&& !model_->bouncingLauncherIcon()) { taskCount = 1; } if (parent_->showTaskManager() && taskCount > 0) { // Show task count indicator. static constexpr int kMaxVisibleTaskCount = 4; if (taskCount > kMaxVisibleTaskCount) { taskCount = kMaxVisibleTaskCount; } auto activeTask = getActiveTask(); if (activeTask > kMaxVisibleTaskCount - 1) { activeTask = kMaxVisibleTaskCount - 1; } // Size (width if horizontal, or height if vertical) of the indicator. const int size = parent_->isGlass() ? DockPanel::kIndicatorSizeGlass : parent_->isFlat2D() ? DockPanel::kIndicatorSizeFlat2D : DockPanel::kIndicatorSizeMetal2D; const auto spacing = DockPanel::kIndicatorSpacing; const auto totalSize = taskCount * size + (taskCount - 1) * spacing; auto x = left_ + (getWidth() - totalSize) / 2 + size / 2; auto y = top_ + (getHeight() - totalSize) / 2 + size / 2; for (int i = 0; i < taskCount; ++i) { // If bouncing launcher icon is not enabled, we use active color // to provide feedback. bool useActiveColor = (i == activeTask) || attentionStrong_ || (launching_ && !model_->bouncingLauncherIcon()); if (parent_->isGlass()) { const auto baseColor = useActiveColor ? model_->activeIndicatorColor() : model_->inactiveIndicatorColor(); drawIndicator(orientation_, x, parent_->taskIndicatorPos(), parent_->taskIndicatorPos(), y, size, DockPanel::k3DPanelThickness, baseColor, painter); } else if (parent_->isFlat2D()) { const auto baseColor = useActiveColor ? model_->activeIndicatorColor2D() : model_->inactiveIndicatorColor2D(); drawIndicatorFlat2D(orientation_, x, parent_->taskIndicatorPos(), parent_->taskIndicatorPos(), y, size, baseColor, painter); } else { // Metal 2D. const auto baseColor = useActiveColor ? model_->activeIndicatorColorMetal2D() : model_->inactiveIndicatorColorMetal2D(); drawIndicatorMetal2D(parent_->position(), x, parent_->taskIndicatorPos(), parent_->taskIndicatorPos(), y, size, baseColor, painter); } x += (size + spacing); y += (size + spacing); } } painter->setRenderHint(QPainter::Antialiasing, false); painter->restore(); painter->save(); if (bouncing_) { float bounceOffset = getBounceOffset(); if (isHorizontal()) { painter->translate(0, bounceOffset); } else { painter->translate(bounceOffset, 0); } } IconBasedDockItem::draw(painter); if (!model_->groupTasksByApplication() && !tasks_.empty() && parent_->itemCount(appId_) > 1) { QString letter; for (auto i = 0; i < label_.size(); ++i) { if (label_.at(i).isLetter()) { letter = label_.at(i).toUpper(); break; } } QFont font; font.setPixelSize(getHeight() / 2); painter->setFont(font); drawBorderedText(left_ + getWidth() * 5 / 8, top_ + getHeight() * 3 / 8, getWidth() / 2, getHeight() * 5 / 8, 0, letter, 2, Qt::black, Qt::white, painter); } painter->restore(); } void Program::mousePressEvent(QMouseEvent* e) { if (e->button() == Qt::LeftButton) { // Run the application. if (appId_ == kLockScreenId) { parent_->leaveEvent(nullptr); QTimer::singleShot(DockPanel::kExecutionDelayMs, [this]() { launch(); }); } else if (appId_ == kShowDesktopId) { WindowSystem::setShowingDesktop(!WindowSystem::showingDesktop()); } else { if (tasks_.empty()) { launch(); startBounceAnimation(); } else { const auto mod = QGuiApplication::keyboardModifiers(); if (mod & Qt::ShiftModifier) { launch(); startBounceAnimation(); } else { cycleThroughTasks(!(mod & Qt::ControlModifier)); } } } } else if (e->button() == Qt::RightButton) { showPopupMenu(&menu_); } else if (e->button() == Qt::MiddleButton) { launch(); startBounceAnimation(); } } void Program::wheelEvent(QWheelEvent* e) { const int delta = e->angleDelta().y(); if (delta == 0) { return; } cycleThroughTasks(delta < 0); } void Program::cycleThroughTasks(bool forward) { if (tasks_.empty()) { return; } const auto activeTask = getActiveTask(); const auto lastTask = static_cast(tasks_.size() - 1); if (activeTask >= 0) { // Cycles through tasks. int nextTask; if (forward) { nextTask = (activeTask < lastTask) ? (activeTask + 1) : -1; } else { nextTask = activeTask > 0 ? (activeTask - 1) : -1; } if (nextTask >= 0 && nextTask <= lastTask) { WindowSystem::activateWindow(tasks_[nextTask].window); } else { for (int i = 0; i <= lastTask; ++i) { WindowSystem::minimizeWindow(tasks_[i].window); } } } else { const auto nextTask = forward ? 0 : lastTask; WindowSystem::activateWindow(tasks_[nextTask].window); } } QString Program::getLabel() const { const unsigned taskCount = tasks_.size(); return (taskCount > 1) ? label_ + " (" + QString::number(tasks_.size()) + " windows)" : label_; } bool Program::addTask(const WindowInfo* task) { if (!model_->groupTasksByApplication() && !tasks_.empty()) { return false; } auto* app = model_->findApplication(task->appId); if ((app && app->appId == appId_) || task->appId == appId_.toStdString()) { tasks_.push_back(ProgramTask(task->window, QString::fromStdString(task->title), task->demandsAttention)); if (task->demandsAttention) { setDemandsAttention(true); } updateMenu(); if (!model_->groupTasksByApplication()) { setLabel(QString::fromStdString(task->title)); } return true; } return false; } bool Program::updateTask(const WindowInfo* task) { if (task->appId != appId_.toStdString()) { return false; } for (auto& existingTask : tasks_) { if (existingTask.window == task->window) { existingTask.demandsAttention = task->demandsAttention; updateDemandsAttention(); return true; } } return false; } bool Program::removeTask(void* window) { for (int i = 0; i < static_cast(tasks_.size()); ++i) { if (tasks_[i].window == window) { tasks_.erase(tasks_.begin() + i); updateMenu(); return true; } } return false; } bool Program::hasTask(void* window) { for (const auto& task : tasks_) { if (task.window == window) { return true; } } return false; } bool Program::beforeTask(const QString& program) { return (pinned_ && appLabel_ != program) || appLabel_ < program; } bool Program::shouldBeRemoved() { if (!tasks_.empty()) { return false; } if (model_->groupTasksByApplication()) { return !pinned_; } else { return !pinned_ || parent_->itemCount(appId_) > 1; } } void Program::launch() { launching_ = true; parent_->update(); launch(command_); QTimer::singleShot(kLaunchingAcknowledgementDurationMs, [this] { launching_ = false; parent_->update(); }); } void Program::pinUnpin() { pinned_ = !pinned_; if (pinned_) { model_->addLauncher(parent_->dockId(), LauncherConfig(appId_, label_, iconName_, command_)); } else { // !pinned model_->removeLauncher(parent_->dockId(), appId_); if (shouldBeRemoved()) { parent_->delayedRefresh(); } } parent_->updatePinnedStatus(appId_, pinned_); } void Program::launch(const QString& command) { QStringList list = QProcess::splitCommand(command); QProcess process; process.setProgram(list.at(0)); process.setArguments(list.mid(1)); auto env = QProcessEnvironment::systemEnvironment(); // Unset XDG_ACTIVATION_TOKEN. env.insert("XDG_ACTIVATION_TOKEN", ""); // Unset layer-shell env. env.insert("QT_WAYLAND_SHELL_INTEGRATION", ""); process.setProcessEnvironment(env); process.setWorkingDirectory(QDir::homePath()); if (!process.startDetached()) { QMessageBox warning(QMessageBox::Warning, "Error", QString("Could not run command: ") + command, QMessageBox::Ok, nullptr, Qt::Tool); warning.exec(); } } void Program::closeAllWindows() { for (const auto& task : tasks_) { WindowSystem::closeWindow(task.window); } } void Program::createMenu() { menu_.addSection(QIcon::fromTheme(iconName_), label_); if (isAppMenuEntry_ || pinned_) { pinAction_ = menu_.addAction( QString("Pinned"), this, [this] { pinUnpin(); }); pinAction_->setCheckable(true); pinAction_->setChecked(pinned_); } if (isAppMenuEntry_) { menu_.addAction(QIcon::fromTheme("list-add"), QString("&New Window"), this, [this] { launch(); }); } closeAction_ = menu_.addAction(QIcon::fromTheme("window-close"), QString("&Close Window"), this, [this] { parent_->minimize(); QTimer::singleShot(DockPanel::kExecutionDelayMs, [this]{ closeAllWindows(); }); }); menu_.addSeparator(); menu_.addAction(QIcon::fromTheme("configure"), QString("Edit &Launchers"), parent_, [this] { parent_->minimize(); QTimer::singleShot(DockPanel::kExecutionDelayMs, [this]{ parent_->showEditLaunchersDialog(); }); }); if (model_->showTaskManager(parent_->dockId())) { menu_.addAction(QIcon::fromTheme("configure"), QString("Task Manager &Settings"), parent_, [this] { parent_->minimize(); QTimer::singleShot(DockPanel::kExecutionDelayMs, [this]{ parent_->showTaskManagerSettingsDialog(); }); }); } menu_.addSeparator(); parent_->addPanelSettings(&menu_); updateMenu(); } void Program::setDemandsAttention(bool value) { if (demandsAttention_ == value) { return; } demandsAttention_ = value; if (demandsAttention_) { animationTimer_.start(); } else if (animationTimer_.isActive()) { animationTimer_.stop(); attentionStrong_ = false; } parent_->update(); } void Program::updateDemandsAttention() { for (const auto& task : tasks_) { if (task.demandsAttention) { setDemandsAttention(true); return; } } setDemandsAttention(false); } void Program::updateMenu() { closeAction_->setVisible(!tasks_.empty()); closeAction_->setText(tasks_.size() > 1 ? "&Close All Windows" : "&Close Window"); } void Program::startBounceAnimation() { if (!model_->bouncingLauncherIcon()) { return; } if (!bouncing_) { bouncing_ = true; bouncingUp_ = true; bounceProgress_ = 0.0f; setAnimationStartAsCurrent(); bounceTimer_.start(); } } void Program::updateBounceAnimation() { if (!bouncing_) { return; } float bounceStep = 1.0f / kBounceSteps; float nextBounceRatio = bounceProgress_ + bounceStep; if (nextBounceRatio < 1.0f) { bounceProgress_ = nextBounceRatio; } else { if (!bouncingUp_) { // Done and done bounceProgress_ = 1.0f; bouncing_ = false; bounceTimer_.stop(); return; } // It was bouncing up bounceProgress_ = 0.0f; bouncingUp_ = false; } parent_->update(); } float Program::getBounceOffset() const { float bounceOffset; if (bouncingUp_) { float ratio = 1.0f - std::pow(1.0f - bounceProgress_, kBounceEaseOut); bounceOffset = -kBounceHeight * ratio; } else { float ratio = std::pow(bounceProgress_, kBounceEaseIn); bounceOffset = -kBounceHeight * (1.0f - ratio); } return bounceOffset; } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/program.h000066400000000000000000000105471512233520000212770ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_PROGRAM_H_ #define CRYSTALDOCK_PROGRAM_H_ #include #include #include #include #include #include "display/window_system.h" #include "icon_based_dock_item.h" namespace crystaldock { struct ProgramTask { void* window; QString name; // e.g. home -- Dolphin bool demandsAttention; ProgramTask(void* window2, QString name2, bool demandsAttention2) : window(window2), name(name2), demandsAttention(demandsAttention2) {} }; class Program : public QObject, public IconBasedDockItem { Q_OBJECT public: Program(DockPanel* parent, MultiDockModel* model, const QString& appId, const QString& label, Qt::Orientation orientation, const QPixmap& icon, int minSize, int maxSize, const QString& command, bool isAppMenuEntry, bool pinned); Program(DockPanel* parent, MultiDockModel* model, const QString& appId, const QString& label, Qt::Orientation orientation, const QPixmap& icon, int minSize, int maxSize); void init(); ~Program() override = default; void draw(QPainter* painter) const override; void mousePressEvent(QMouseEvent* e) override; void wheelEvent(QWheelEvent* e) override; QString getLabel() const override; QString getAppId() const override { return appId_; } QString getAppLabel() const override { return appLabel_; } void updatePinnedStatus(bool pinned) override { pinned_ = pinned; pinAction_->setChecked(pinned_); } bool addTask(const WindowInfo* task) override; bool updateTask(const WindowInfo* task) override; bool removeTask(void* window) override; bool hasTask(void* window) override; bool beforeTask(const QString& program) override; bool shouldBeRemoved() override; virtual void maybeResetActiveWindow(QMouseEvent* e) override { if (e->button() != Qt::LeftButton) { WindowSystem::resetActiveWindow(); } } void setDemandsAttention(bool demandsAttention) override; int taskCount() const { return static_cast(tasks_.size()); } bool active() const { return getActiveTask() >= 0; } int getActiveTask() const { for (int i = 0; i < static_cast(tasks_.size()); ++i) { if (WindowSystem::activeWindow() == tasks_[i].window) { return i; } } return -1; } bool pinned() { return pinned_; } void pinUnpin(); void launch(); static void launch(const QString& command); void closeAllWindows(); private: static constexpr int kLaunchingAcknowledgementDurationMs = 3000; // 3 seconds. // For bounce animation. static constexpr int kBounceHeight = 32; static constexpr int kBounceSteps = 12; static constexpr int kBounceIntervalMs = 25; static constexpr float kBounceEaseIn = 2.0f; static constexpr float kBounceEaseOut = 2.0f; void createMenu(); void updateDemandsAttention(); void updateMenu(); void cycleThroughTasks(bool forward); QString appId_; QString appLabel_; QString command_; // Is an entry on the App Menu, exluding system commands such as Lock Screen / Shut Down. bool isAppMenuEntry_; bool pinned_; std::vector tasks_; // Context (right-click) menu. QMenu menu_; QAction* pinAction_; QAction* closeAction_; // Demands attention logic. bool demandsAttention_; QTimer animationTimer_; bool attentionStrong_; // For launching acknowledgement. bool launching_; QTimer bounceTimer_; bool bouncing_ = false; float bounceProgress_ = 0.0f; bool bouncingUp_ = true; void startBounceAnimation(); void updateBounceAnimation(); float getBounceOffset() const; friend class DockPanel; }; } // namespace crystaldock #endif // CRYSTALDOCK_PROGRAM_H_ dangvd-crystal-dock-7a34e96/src/view/separator.cc000066400000000000000000000037741512233520000217720ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "separator.h" #include "dock_panel.h" namespace crystaldock { constexpr float Separator::kWhRatio; Separator::Separator(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize, bool isLauncherSeparator) : IconlessDockItem(parent, model, "" /* label */, orientation, minSize, maxSize, kWhRatio, /*reverseWhRatio=*/ true), isLauncherSeparator_(isLauncherSeparator) { whRatio_ = 0.5; } void Separator::draw(QPainter* painter) const { int x, y, w, h; if (orientation_ == Qt::Horizontal) { x = left_ + getWidth() / 2; y = (parent_->position() == PanelPosition::Top) ? top_ : getHeight() - getMinHeight() + top_; w = 1; h = getMinHeight(); } else { // Vertical. x = (parent_->position() == PanelPosition::Left) ? left_ : getWidth() - getMinWidth() + left_; y = top_ + getHeight() / 2; w = getMinWidth(); h = 1; } if (parent_->isGlass()) { // For Glass 2D/3D styles, do not draw anything. } else if (parent_->isFlat2D()) { painter->fillRect(x, y, w, h, model_->backgroundColor2D().lighter()); } else { // Metal 2D. painter->fillRect(x, y, w, h, model_->borderColor()); } } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/separator.h000066400000000000000000000033041512233520000216210ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_SEPARATOR_H_ #define CRYSTALDOCK_SEPARATOR_H_ #include "iconless_dock_item.h" namespace crystaldock { // A digital Separator. class Separator : public QObject, public IconlessDockItem { Q_OBJECT public: Separator(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize, bool isLauncherSeparator); virtual ~Separator() = default; void draw(QPainter* painter) const override; void mousePressEvent(QMouseEvent* e) override { /* no-op */ } bool beforeTask(const QString& program) override { return isLauncherSeparator_; } QString getAppId() const override { return isLauncherSeparator_ ? kLauncherSeparatorId : kSeparatorId; } private: static constexpr float kWhRatio = 0.1; // A Launcher Separator will push task icons, that do not belong to pinned // applications, behind it. bool isLauncherSeparator_ = false; }; } // namespace crystaldock #endif // CRYSTALDOCK_SEPARATOR_H_ dangvd-crystal-dock-7a34e96/src/view/task_manager_settings_dialog.cc000066400000000000000000000050511512233520000256530ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "task_manager_settings_dialog.h" #include "ui_task_manager_settings_dialog.h" #include #include namespace crystaldock { TaskManagerSettingsDialog::TaskManagerSettingsDialog(QWidget* parent, MultiDockModel* model) : QDialog(parent), ui(new Ui::TaskManagerSettingsDialog), model_(model), isSingleScreen_(true) { ui->setupUi(this); // Adjust the UI for single/multi-screen. isSingleScreen_ = (WindowSystem::screens().size() == 1); ui->showCurrentScreenOnly->setVisible(!isSingleScreen_); if (isSingleScreen_) { ui->groupTasksByApplication->move(40, 130); ui->buttonBox->move(40, 200); resize(600, 270); } connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonClicked(QAbstractButton*))); loadData(); } TaskManagerSettingsDialog::~TaskManagerSettingsDialog() { delete ui; } void TaskManagerSettingsDialog::accept() { QDialog::accept(); saveData(); } void TaskManagerSettingsDialog::buttonClicked(QAbstractButton* button) { auto role = ui->buttonBox->buttonRole(button); if (role == QDialogButtonBox::ApplyRole) { saveData(); } } void TaskManagerSettingsDialog::loadData() { ui->showCurrentDesktopOnly->setChecked(model_->currentDesktopTasksOnly()); if (!isSingleScreen_) { ui->showCurrentScreenOnly->setChecked(model_->currentScreenTasksOnly()); } ui->groupTasksByApplication->setChecked(model_->groupTasksByApplication()); } void TaskManagerSettingsDialog::saveData() { model_->setCurrentDesktopTasksOnly(ui->showCurrentDesktopOnly->isChecked()); if (!isSingleScreen_) { model_->setCurrentScreenTasksOnly(ui->showCurrentScreenOnly->isChecked()); } model_->setGroupTasksByApplication(ui->groupTasksByApplication->isChecked()); model_->saveAppearanceConfig(); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/task_manager_settings_dialog.h000066400000000000000000000030101512233520000255060ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_TASK_MANAGER_SETTINGS_DIALOG_H_ #define CRYSTALDOCK_TASK_MANAGER_SETTINGS_DIALOG_H_ #include #include #include namespace Ui { class TaskManagerSettingsDialog; } namespace crystaldock { class TaskManagerSettingsDialog : public QDialog { Q_OBJECT public: explicit TaskManagerSettingsDialog(QWidget* parent, MultiDockModel* model); ~TaskManagerSettingsDialog(); void reload() { loadData(); } public slots: void accept() override; void buttonClicked(QAbstractButton* button); private: void loadData(); void saveData(); Ui::TaskManagerSettingsDialog *ui; MultiDockModel* model_; bool isSingleScreen_; }; } // namespace crystaldock #endif // CRYSTALDOCK_TASK_MANAGER_SETTINGS_DIALOG_H_ dangvd-crystal-dock-7a34e96/src/view/task_manager_settings_dialog.ui000066400000000000000000000057161512233520000257130ustar00rootroot00000000000000 TaskManagerSettingsDialog 0 0 600 320 Task Manager Settings 30 250 530 32 Qt::Orientation::Horizontal QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok true 40 30 351 28 Showing tasks from: 80 80 401 32 Current desktop only true 80 130 421 32 Current screen only true 40 180 401 30 Group tasks by application buttonBox accepted() TaskManagerSettingsDialog accept() 248 254 157 274 buttonBox rejected() TaskManagerSettingsDialog reject() 316 260 286 274 dangvd-crystal-dock-7a34e96/src/view/trash.cc000066400000000000000000000160041512233520000211010ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "trash.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "dock_panel.h" #include "program.h" namespace crystaldock { Trash::Trash(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize) : IconBasedDockItem(parent, model, "Trash", orientation, kEmptyTrashIconName, minSize, maxSize), trashWatcher_(nullptr), isEmpty_(true), acceptingDrop_(false) { trashPath_ = getTrashPath(); trashInfoPath_ = getTrashInfoPath(); trashFilesPath_ = getTrashFilesPath(); createMenu(); setupTrashWatcher(); updateTrashState(); connect(&menu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); } void Trash::draw(QPainter* painter) const { IconBasedDockItem::draw(painter); if (acceptingDrop_) { painter->save(); QPen pen(QColor(0, 150, 255, 200), 2); painter->setPen(pen); painter->setBrush(QBrush(QColor(0, 150, 255, 50))); painter->drawRoundedRect(left_, top_, getWidth(), getHeight(), 8, 8); painter->restore(); } } void Trash::mousePressEvent(QMouseEvent* e) { if (e->button() == Qt::LeftButton) { openTrash(); } else if (e->button() == Qt::RightButton) { showPopupMenu(&menu_); } } QString Trash::getLabel() const { return isEmpty_ ? "Trash (Empty)" : "Trash (Full)"; } void Trash::updateTrashState() { bool wasEmpty = isEmpty_; isEmpty_ = isTrashEmpty(); if (isEmpty_ != wasEmpty) { updateIcon(); parent_->update(); } } void Trash::emptyTrash() { if (isEmpty_) { return; } QDir trashFilesDir(trashFilesPath_); QDir trashInfoDir(trashInfoPath_); trashFilesDir.removeRecursively(); trashInfoDir.removeRecursively(); QDir().mkpath(trashFilesPath_); QDir().mkpath(trashInfoPath_); updateTrashState(); } void Trash::openTrash() { QProcess* process = new QProcess(parent_); connect(process, QOverload::of(&QProcess::finished), [this, process](int exitCode, QProcess::ExitStatus exitStatus) { // Tries to find the default file manager. Falls back to using "xdg-open". QString command = "xdg-open"; if (exitCode == 0) { QString desktopFile = process->readAllStandardOutput().trimmed(); if (desktopFile.endsWith(".desktop")) { desktopFile = desktopFile.first(desktopFile.size() - 8); } const auto* fileManager = model_->findApplication(desktopFile.toStdString()); if (fileManager != nullptr) { command = fileManager->command; } } Program::launch(command + " trash:/"); process->deleteLater(); }); process->start("xdg-mime", QStringList() << "query" << "default" << "inode/directory"); } void Trash::setAcceptDrops(bool accept) { acceptingDrop_ = accept; parent_->update(); } bool Trash::canAcceptDrop(const QMimeData* mimeData) const { return mimeData->hasUrls(); } void Trash::dragEnterEvent(QDragEnterEvent* event) { if (canAcceptDrop(event->mimeData())) { event->acceptProposedAction(); setAcceptDrops(true); } } void Trash::dropEvent(QDropEvent* event) { setAcceptDrops(false); if (!canAcceptDrop(event->mimeData())) { return; } QStringList filePaths; for (const QUrl& url : event->mimeData()->urls()) { if (url.isLocalFile()) { filePaths << url.toLocalFile(); } } if (!filePaths.isEmpty()) { moveToTrash(filePaths); event->acceptProposedAction(); } } void Trash::createMenu() { menu_.addSection(label_); emptyTrashAction_ = menu_.addAction(QIcon::fromTheme("trash-empty"), "Empty Trash"); connect(emptyTrashAction_, &QAction::triggered, this, &Trash::emptyTrash); menu_.addSeparator(); parent_->addPanelSettings(&menu_); } void Trash::moveToTrash(const QStringList& filePaths) { for (const QString& filePath : filePaths) { QFileInfo fileInfo(filePath); if (!fileInfo.exists()) { continue; } QString fileName = fileInfo.fileName(); QString destPath = trashFilesPath_ + "/" + fileName; QString infoPath = trashInfoPath_ + "/" + fileName + ".trashinfo"; int counter = 1; while (QFile::exists(destPath)) { QString baseName = fileInfo.baseName(); QString suffix = fileInfo.completeSuffix(); if (!suffix.isEmpty()) { fileName = QString("%1_%2.%3").arg(baseName).arg(counter).arg(suffix); } else { fileName = QString("%1_%2").arg(baseName).arg(counter); } destPath = trashFilesPath_ + "/" + fileName; infoPath = trashInfoPath_ + "/" + fileName + ".trashinfo"; counter++; } if (QFile::rename(filePath, destPath)) { QFile infoFile(infoPath); if (infoFile.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream stream(&infoFile); stream << "[Trash Info]\n"; stream << "Path=" << filePath << "\n"; stream << "DeletionDate=" << QDateTime::currentDateTime().toString(Qt::ISODate) << "\n"; infoFile.close(); } } } updateTrashState(); } bool Trash::isTrashEmpty() const { QDir trashDir(trashFilesPath_); return trashDir.isEmpty(); } QString Trash::getTrashPath() const { QString dataHome = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); return dataHome + "/Trash"; } QString Trash::getTrashInfoPath() const { return trashPath_ + "/info"; } QString Trash::getTrashFilesPath() const { return trashPath_ + "/files"; } void Trash::updateIcon() { setIconName(isEmpty_ ? kEmptyTrashIconName : kFullTrashIconName); emptyTrashAction_->setEnabled(!isEmpty_); } void Trash::setupTrashWatcher() { trashWatcher_ = new QFileSystemWatcher(this); QDir().mkpath(trashFilesPath_); QDir().mkpath(trashInfoPath_); trashWatcher_->addPath(trashFilesPath_); trashWatcher_->addPath(trashInfoPath_); connect(trashWatcher_, &QFileSystemWatcher::directoryChanged, this, &Trash::updateTrashState); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/trash.h000066400000000000000000000047631512233520000207540ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_TRASH_H_ #define CRYSTALDOCK_TRASH_H_ #include #include #include #include #include #include #include #include #include #include #include "icon_based_dock_item.h" namespace crystaldock { class Trash : public QObject, public IconBasedDockItem { Q_OBJECT public: Trash(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize); virtual ~Trash() = default; void draw(QPainter* painter) const override; void mousePressEvent(QMouseEvent* e) override; QString getLabel() const override; bool beforeTask(const QString& program) override { return false; } QString getAppId() const override { return "trash"; } void setAcceptDrops(bool accept); bool canAcceptDrop(const QMimeData* mimeData) const; public slots: void updateTrashState(); void emptyTrash(); void openTrash(); void dragEnterEvent(QDragEnterEvent* event); void dropEvent(QDropEvent* event); private: void createMenu(); void moveToTrash(const QStringList& filePaths); bool isTrashEmpty() const; QString getTrashPath() const; QString getTrashInfoPath() const; QString getTrashFilesPath() const; void updateIcon(); void setupTrashWatcher(); QString trashPath_; QString trashInfoPath_; QString trashFilesPath_; QFileSystemWatcher* trashWatcher_; QMenu menu_; QAction* emptyTrashAction_; QAction* openTrashAction_; QAction* restoreAction_; bool isEmpty_; bool acceptingDrop_; static constexpr const char* kEmptyTrashIconName = "user-trash"; static constexpr const char* kFullTrashIconName = "user-trash-full"; }; } // namespace crystaldock #endif // CRYSTALDOCK_TRASH_H_ dangvd-crystal-dock-7a34e96/src/view/version_checker.cc000066400000000000000000000145061512233520000231360ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "version_checker.h" #include #include #include #include #include #include #include "dock_panel.h" namespace crystaldock { VersionChecker::VersionChecker(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize) : IconBasedDockItem(parent, model, "Version Checker", orientation, "", minSize, maxSize), infoDialog_(QMessageBox::Information, "Version Information", QString{}, QMessageBox::Ok, parent, Qt::Tool) { createMenu(); const QString version = DockPanel::kVersion; if (version.toLower().contains("alpha")) { setVersionStatus(VersionStatus::Alpha); } else if (version.toLower().contains("beta") || version.toLower().contains("rc")) { setVersionStatus(VersionStatus::Beta); } else { setVersionStatus(VersionStatus::UpToDate); } if (status_ == VersionStatus::UpToDate) { // checks version now and every hour. QTimer::singleShot(1000, this, &VersionChecker::checkVersion); timer_ = new QTimer(this); connect(timer_, &QTimer::timeout, this, &VersionChecker::checkVersion); timer_->start(timerInterval_); } connect(&menu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); } void VersionChecker::mousePressEvent(QMouseEvent* e) { if (e->button() == Qt::LeftButton) { parent_->minimize(); QTimer::singleShot(DockPanel::kExecutionDelayMs, [this]{ showVersionInfo(); }); } else if (e->button() == Qt::RightButton) { showPopupMenu(&menu_); } } void VersionChecker::checkVersion() { if (status_ != VersionStatus::UpToDate) { return; } QProcess* curlProcess = new QProcess(parent_); connect(curlProcess, QOverload::of(&QProcess::finished), [this, curlProcess](int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode == 0) { const QString output = curlProcess->readAllStandardOutput(); const QJsonDocument jsonDoc = QJsonDocument::fromJson(output.toUtf8()); if (jsonDoc.isNull() || !jsonDoc.isObject()) { return; } const QJsonObject json = jsonDoc.object(); if (!json.contains("tag_name")) { return; } QString latestRelease = json.value("tag_name").toString().trimmed(); latestRelease = latestRelease.mid(latestRelease.indexOf("v") + 1); const QString version = DockPanel::kVersion; if (version == latestRelease) { setVersionStatus(VersionStatus::UpToDate); } else { setVersionStatus(VersionStatus::OutOfDate); } } curlProcess->deleteLater(); }); curlProcess->start("curl", QStringList() << "-s" << "https://api.github.com/repos/dangvd/crystal-dock/releases/latest"); } void VersionChecker::setVersionStatus(VersionStatus status) { status_ = status; switch (status_) { case VersionStatus::Alpha: setIconName("dialog-warning"); setLabel("Warning: Alpha version"); infoDialog_.setIcon(QMessageBox::Warning); infoDialog_.setText(QString("

Warning: You are using an alpha version of Crystal Dock. Please use the latest release instead:") + "

https://github.com/dangvd/crystal-dock/releases"); break; case VersionStatus::Beta: setIconName("dialog-warning"); setLabel("Warning: Beta version"); infoDialog_.setIcon(QMessageBox::Warning); infoDialog_.setText(QString("

Warning: You are using a beta version of Crystal Dock. Please use the latest release instead:") + "

https://github.com/dangvd/crystal-dock/releases"); break; case VersionStatus::OutOfDate: setIconName("dialog-warning"); setLabel("Warning: Out-of-date version"); infoDialog_.setIcon(QMessageBox::Warning); infoDialog_.setText(QString("

Warning: You are using an out-of-date version of Crystal Dock. Please use the latest release instead:") + "

https://github.com/dangvd/crystal-dock/releases"); break; case VersionStatus::UpToDate: setIconName("dialog-ok", "dialog-information"); setLabel("Dock version: Up to date"); infoDialog_.setIcon(QMessageBox::Information); infoDialog_.setText(QString("

You are using the latest release of Crystal Dock.")); break; } } void VersionChecker::createMenu() { menu_.addSection("Version Checker"); auto* frequencyMenu = menu_.addMenu("Checking Frequency"); auto* frequencyGroup = new QActionGroup(this); auto* hourlyAction = frequencyMenu->addAction("Hourly", this, [this](){ if (status_ == VersionStatus::UpToDate) { timerInterval_ = 60 * 60 * 1000; timer_->start(timerInterval_); } }); hourlyAction->setCheckable(true); hourlyAction->setActionGroup(frequencyGroup); hourlyAction->setChecked(true); auto* dailyAction = frequencyMenu->addAction("Daily", this, [this](){ if (status_ == VersionStatus::UpToDate) { timerInterval_ = 24 * 60 * 60 * 1000; timer_->start(timerInterval_); } }); dailyAction->setCheckable(true); dailyAction->setActionGroup(frequencyGroup); menu_.addSeparator(); parent_->addPanelSettings(&menu_); } void VersionChecker::showVersionInfo() { infoDialog_.exec(); } } dangvd-crystal-dock-7a34e96/src/view/version_checker.h000066400000000000000000000032361512233520000227760ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_VERSION_CHECKER_H_ #define CRYSTALDOCK_VERSION_CHECKER_H_ #include "icon_based_dock_item.h" #include namespace crystaldock { enum class VersionStatus { Alpha, Beta, OutOfDate, UpToDate }; class VersionChecker : public QObject, public IconBasedDockItem { Q_OBJECT public: VersionChecker(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize); virtual ~VersionChecker() = default; bool beforeTask(const QString& program) override { return false; } void mousePressEvent(QMouseEvent* e) override; private: void checkVersion(); void setVersionStatus(VersionStatus status); void createMenu(); void showVersionInfo(); VersionStatus status_; QMenu menu_; QTimer* timer_ = nullptr; uint32_t timerInterval_ = 60 * 60 * 1000; // hourly. QMessageBox infoDialog_; }; } // namespace crystaldock #endif // CRYSTALDOCK_VERSION_CHECKER_H_ dangvd-crystal-dock-7a34e96/src/view/volume_control.cc000066400000000000000000000254531512233520000230370ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "volume_control.h" #include #include #include #include #include #include #include #include #include #include "dock_panel.h" #include #include namespace crystaldock { constexpr int VolumeControl::kUpdateInterval; VolumeControl::VolumeControl(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize) : IconBasedDockItem(parent, model, "Volume Control", orientation, "audio-volume", minSize, maxSize), updateTimer_(new QTimer(this)), volumeProcess_(nullptr) { createMenu(); // Set up update timer connect(updateTimer_, &QTimer::timeout, this, &VolumeControl::refreshVolumeInfo); updateTimer_->start(kUpdateInterval); // Initial volume info refresh QTimer::singleShot(1000, this, &VolumeControl::refreshVolumeInfo); connect(&menu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); connect(&contextMenu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); } VolumeControl::~VolumeControl() { if (volumeProcess_ && volumeProcess_->state() != QProcess::NotRunning) { volumeProcess_->kill(); volumeProcess_->waitForFinished(1000); } } void VolumeControl::draw(QPainter* painter) const { if (!getIcon(size_).isNull()) { IconBasedDockItem::draw(painter); return; } // Fallback: draw custom speaker icon. const auto x = left_; const auto y = top_; const auto w = getWidth(); const auto h = getHeight(); painter->setRenderHint(QPainter::Antialiasing); painter->setPen(QPen(Qt::white, 2)); painter->setBrush(Qt::white); const int centerX = x + w / 2; const int centerY = y + h / 2; const int speakerSize = qMin(w, h) * 0.4; // Draw speaker cone QRect speakerRect(centerX - speakerSize / 2, centerY - speakerSize / 3, speakerSize / 2, speakerSize * 2 / 3); painter->fillRect(speakerRect, Qt::white); // Draw speaker driver QPolygon driver; driver << QPoint(centerX, centerY - speakerSize / 3) << QPoint(centerX + speakerSize / 2, centerY - speakerSize / 6) << QPoint(centerX + speakerSize / 2, centerY + speakerSize / 6) << QPoint(centerX, centerY + speakerSize / 3); painter->drawPolygon(driver); // Draw volume level arcs if not muted if (!isMuted_ && currentVolume_ > 0) { painter->setBrush(Qt::NoBrush); const int arcStartX = centerX + speakerSize / 2 + 4; const int numArcs = currentVolume_ > 70 ? 3 : currentVolume_ > 30 ? 2 : 1; for (int i = 0; i < numArcs; ++i) { const int arcRadius = speakerSize / 4 + i * speakerSize / 8; const QRect arcRect(arcStartX - arcRadius, centerY - arcRadius, arcRadius * 2, arcRadius * 2); painter->drawArc(arcRect, -30 * 16, 60 * 16); } } // Draw mute X if muted if (isMuted_) { painter->setPen(QPen(Qt::red, 3)); const int crossSize = speakerSize / 2; painter->drawLine(centerX - crossSize / 2, centerY - crossSize / 2, centerX + crossSize / 2, centerY + crossSize / 2); painter->drawLine(centerX + crossSize / 2, centerY - crossSize / 2, centerX - crossSize / 2, centerY + crossSize / 2); } } void VolumeControl::mousePressEvent(QMouseEvent* e) { if (e->button() == Qt::LeftButton) { if (commandExists({kCommand}).isEmpty()) { QMessageBox::warning(parent_, "Command not found", "Command 'pactl' not found. This is required by the Volume Control component."); return; } showPopupMenu(&menu_); } else if (e->button() == Qt::MiddleButton) { toggleMute(); } else if (e->button() == Qt::RightButton) { showPopupMenu(&contextMenu_); } } void VolumeControl::wheelEvent(QWheelEvent* e) { if (e->angleDelta().y() == 0) { return; } const int delta = e->angleDelta().y(); const int scrollStep = model_->volumeScrollStep(); const int volumeChange = (delta > 0) ? scrollStep : -scrollStep; const int newVolume = qBound(0, currentVolume_ + volumeChange, 100); // Update slider position without triggering signals volumeSlider_->blockSignals(true); volumeSlider_->setValue(newVolume); volumeSlider_->blockSignals(false); } QString VolumeControl::getLabel() const { return isMuted_ ? "Volume: Muted" : QString("Volume: %1%").arg(currentVolume_); } void VolumeControl::refreshVolumeInfo() { // Prevent concurrent processes if (volumeProcess_ && volumeProcess_->state() != QProcess::NotRunning) { return; } // Get volume level volumeProcess_ = new QProcess(parent_); connect(volumeProcess_, QOverload::of(&QProcess::finished), [this](int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode == 0) { const QString output = volumeProcess_->readAllStandardOutput(); QRegularExpression volumeRegex(R"((\d+)%)"); QRegularExpressionMatch match = volumeRegex.match(output); if (match.hasMatch()) { const int newVolume = match.captured(1).toInt(); if (newVolume != currentVolume_) { currentVolume_ = newVolume; updateUi(); } } } volumeProcess_->deleteLater(); volumeProcess_ = nullptr; // Check mute status QProcess* muteProcess = new QProcess(parent_); connect(muteProcess, QOverload::of(&QProcess::finished), [this, muteProcess](int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode == 0) { const QString output = muteProcess->readAllStandardOutput().trimmed(); const bool newMuted = (output.toLower().contains("yes")); if (newMuted != isMuted_) { isMuted_ = newMuted; updateUi(); } } muteProcess->deleteLater(); }); muteProcess->start(kCommand, QStringList() << "get-sink-mute" << "@DEFAULT_SINK@"); }); volumeProcess_->start(kCommand, QStringList() << "get-sink-volume" << "@DEFAULT_SINK@"); } void VolumeControl::setVolume(int volume) { QProcess* process = new QProcess(parent_); connect(process, QOverload::of(&QProcess::finished), [process](int exitCode, QProcess::ExitStatus exitStatus) { process->deleteLater(); }); currentVolume_ = volume; updateUi(); process->start(kCommand, QStringList() << "set-sink-volume" << "@DEFAULT_SINK@" << QString("%1%").arg(volume)); } void VolumeControl::onVolumeSliderChanged(int value) { setVolume(value); parent_->update(); } void VolumeControl::toggleMute() { QProcess* process = new QProcess(parent_); connect(process, QOverload::of(&QProcess::finished), [process](int exitCode, QProcess::ExitStatus exitStatus) { process->deleteLater(); }); isMuted_ = !isMuted_; updateUi(); process->start(kCommand, QStringList() << "set-sink-mute" << "@DEFAULT_SINK@" << "toggle"); } void VolumeControl::createMenu() { // Volume menu // Volume slider volumeSlider_ = new QSlider(Qt::Horizontal); volumeSlider_->setRange(0, 100); volumeSlider_->setValue(currentVolume_); volumeSlider_->setMinimumWidth(getMaxWidth()); connect(volumeSlider_, &QSlider::valueChanged, this, &VolumeControl::onVolumeSliderChanged); QWidgetAction* sliderAction = new QWidgetAction(&menu_); sliderAction->setDefaultWidget(volumeSlider_); menu_.addAction(sliderAction); // Mute toggle muteAction_ = menu_.addAction("Mute", this, &VolumeControl::toggleMute); muteAction_->setCheckable(true); // Context menu contextMenu_.addSection("Volume Control"); // Scroll step submenu scrollStepMenu_ = contextMenu_.addMenu("Volume Scroll Step"); scrollStepGroup_ = new QActionGroup(this); scrollStep1Action_ = scrollStepMenu_->addAction("1% (Fine)", this, &VolumeControl::setVolumeScrollStep1); scrollStep1Action_->setCheckable(true); scrollStep1Action_->setActionGroup(scrollStepGroup_); scrollStep2Action_ = scrollStepMenu_->addAction("2% (Default)", this, &VolumeControl::setVolumeScrollStep2); scrollStep2Action_->setCheckable(true); scrollStep2Action_->setActionGroup(scrollStepGroup_); scrollStep2Action_->setChecked(true); scrollStep5Action_ = scrollStepMenu_->addAction("5% (Coarse)", this, &VolumeControl::setVolumeScrollStep5); scrollStep5Action_->setCheckable(true); scrollStep5Action_->setActionGroup(scrollStepGroup_); scrollStep10Action_ = scrollStepMenu_->addAction("10% (Very Coarse)", this, &VolumeControl::setVolumeScrollStep10); scrollStep10Action_->setCheckable(true); scrollStep10Action_->setActionGroup(scrollStepGroup_); contextMenu_.addSeparator(); parent_->addPanelSettings(&contextMenu_); } void VolumeControl::setVolumeScrollStep1() { model_->setVolumeScrollStep(1); model_->saveAppearanceConfig(true); } void VolumeControl::setVolumeScrollStep2() { model_->setVolumeScrollStep(2); model_->saveAppearanceConfig(true); } void VolumeControl::setVolumeScrollStep5() { model_->setVolumeScrollStep(5); model_->saveAppearanceConfig(true); } void VolumeControl::setVolumeScrollStep10() { model_->setVolumeScrollStep(10); model_->saveAppearanceConfig(true); } void VolumeControl::updateUi() { volumeSlider_->blockSignals(true); volumeSlider_->setValue(currentVolume_); volumeSlider_->blockSignals(false); muteAction_->setChecked(isMuted_); if (isMuted_ || currentVolume_ == 0) { setIconName("audio-volume-muted"); } else if (currentVolume_ < 30) { setIconName("audio-volume-low"); } else if (currentVolume_ <= 70) { setIconName("audio-volume-medium"); } else { setIconName("audio-volume-high"); } parent_->update(); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/volume_control.h000066400000000000000000000053111512233520000226700ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_VOLUME_CONTROL_H_ #define CRYSTALDOCK_VOLUME_CONTROL_H_ #include "icon_based_dock_item.h" #include #include #include #include #include #include #include #include #include namespace crystaldock { // A volume control widget that integrates with PulseAudio. class VolumeControl : public QObject, public IconBasedDockItem { Q_OBJECT public: VolumeControl(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize); virtual ~VolumeControl(); void draw(QPainter* painter) const override; void mousePressEvent(QMouseEvent* e) override; void wheelEvent(QWheelEvent* e) override; QString getLabel() const override; bool beforeTask(const QString& program) override { return false; } public slots: void refreshVolumeInfo(); void onVolumeSliderChanged(int value); void toggleMute(); void setVolumeScrollStep1(); void setVolumeScrollStep2(); void setVolumeScrollStep5(); void setVolumeScrollStep10(); private: static constexpr char kCommand[] = "pactl"; static constexpr int kUpdateInterval = 1000; // Creates the context menu. void createMenu(); // PulseAudio volume control operations. void setVolume(int volume); void updateUi(); // Current volume state. int currentVolume_ = 50; bool isMuted_ = false; // Update timer. QTimer* updateTimer_; // PulseAudio process for monitoring volume. QProcess* volumeProcess_; // Left-click volume menu. QMenu menu_; QSlider* volumeSlider_; // Right-click context menu. QMenu contextMenu_; QAction* muteAction_; QAction* audioSettingsAction_; // Scroll step submenu. QMenu* scrollStepMenu_; QAction* scrollStep1Action_; QAction* scrollStep2Action_; QAction* scrollStep5Action_; QAction* scrollStep10Action_; QActionGroup* scrollStepGroup_; }; } // namespace crystaldock #endif // CRYSTALDOCK_VOLUME_CONTROL_H_ dangvd-crystal-dock-7a34e96/src/view/wallpaper_settings_dialog.cc000066400000000000000000000124641512233520000252140ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "wallpaper_settings_dialog.h" #include "ui_wallpaper_settings_dialog.h" #include #include #include #include #include #include #include #include #include "display/window_system.h" namespace crystaldock { WallpaperSettingsDialog::WallpaperSettingsDialog(QWidget* parent, MultiDockModel* model) : QDialog(parent), ui(new Ui::WallpaperSettingsDialog), model_(model), desktopEnv_(DesktopEnv::getDesktopEnv()), currentDir_(QDir::homePath()), multiScreen_(false) { ui->setupUi(this); setWindowFlag(Qt::Tool); // Populate screen list. const int screenCount = WindowSystem::screens().size(); for (int i = 1; i <= screenCount; ++i) { ui->screen->addItem(QString::number(i)); } ui->screen->setCurrentIndex(0); // Adjust the UI for single/multi-screen. multiScreen_ = (screenCount > 1) && desktopEnv_->supportSeparateSreenWallpapers(); ui->screenLabel->setVisible(multiScreen_); ui->screen->setVisible(multiScreen_); adjustUiForScreen(); connect(ui->desktop, SIGNAL(currentIndexChanged(int)), this, SLOT(reload())); connect(ui->browse, SIGNAL(clicked()), this, SLOT(browseWallpaper())); connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonClicked(QAbstractButton*))); if (multiScreen_) { connect(ui->screen, SIGNAL(currentIndexChanged(int)), this, SLOT(reload())); } } WallpaperSettingsDialog::~WallpaperSettingsDialog() { delete ui; } void WallpaperSettingsDialog::setFor(int desktop, int screen) { populateDesktopList(); ui->desktop->setCurrentIndex(desktop - 1); if (multiScreen_) { ui->screen->setCurrentIndex(screen); adjustUiForScreen(); } loadData(); } void WallpaperSettingsDialog::populateDesktopList() { ui->desktop->clear(); for (const auto& desktop : WindowSystem::desktops()) { ui->desktop->addItem(QString::number(desktop.number), QVariant(QString::fromStdString(desktop.id))); } } void WallpaperSettingsDialog::accept() { QDialog::accept(); saveData(); } void WallpaperSettingsDialog::buttonClicked(QAbstractButton *button) { auto role = ui->buttonBox->buttonRole(button); if (role == QDialogButtonBox::ApplyRole) { saveData(); } } void WallpaperSettingsDialog::browseWallpaper() { const QString& wallpaper = QFileDialog::getOpenFileName( this, QString("Select Wallpaper Image"), currentDir_, QString("Image Files (*.png *.jpg *.bmp)")); if (wallpaper.isEmpty()) { return; } wallpaper_ = wallpaper; ui->preview->setPixmap(QPixmap(wallpaper_)); currentDir_ = QFileInfo(wallpaper_).dir().absolutePath(); } void WallpaperSettingsDialog::adjustUiForScreen() { const auto screenGeometry = WindowSystem::screens()[screen()]->geometry(); const int w = ui->preview->width(); const int h = w * screenGeometry.height() / screenGeometry.width(); const int delta = h - ui->preview->height(); ui->preview->resize(w, h); ui->previewHolder->resize(ui->previewHolder->width(), ui->previewHolder->height() + delta); ui->buttonBox->move(ui->buttonBox->x(), ui->buttonBox->y() + delta); resize(width(), height() + delta); } void WallpaperSettingsDialog::reload() { if (multiScreen_) { adjustUiForScreen(); } loadData(); } int WallpaperSettingsDialog::screen() const { return ui->screen->currentIndex(); } std::string WallpaperSettingsDialog::desktop() const { return ui->desktop->currentData().toString().toStdString(); } void WallpaperSettingsDialog::loadData() { wallpaper_ = model_->wallpaper(desktop(), screen()); ui->preview->setPixmap(QPixmap(wallpaper_)); } void WallpaperSettingsDialog::saveData() { if (!wallpaper_.isEmpty() && (wallpaper_ != model_->wallpaper(desktop(), screen()))) { const int screenCount = WindowSystem::screens().size(); if (desktopEnv_->supportSeparateSreenWallpapers()) { model_->setWallpaper(desktop(), screen(), wallpaper_); } else { for (int screen = 0; screen < screenCount; ++screen) { model_->setWallpaper(desktop(), screen, wallpaper_); } } model_->saveAppearanceConfig(); if (desktop() == WindowSystem::currentDesktop()) { if (desktopEnv_->supportSeparateSreenWallpapers()) { model_->notifyWallpaperChanged(screen()); } else { for (int screen = 0; screen < screenCount; ++screen) { model_->notifyWallpaperChanged(screen); } } } } } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/wallpaper_settings_dialog.h000066400000000000000000000037211512233520000250520ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2022 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTALDOCK_WALLPAPER_SETTINGS_DIALOG_H_ #define CRYSTALDOCK_WALLPAPER_SETTINGS_DIALOG_H_ #include #include #include #include #include namespace Ui { class WallpaperSettingsDialog; } namespace crystaldock { class WallpaperSettingsDialog : public QDialog { Q_OBJECT public: WallpaperSettingsDialog(QWidget* parent, MultiDockModel* model); ~WallpaperSettingsDialog(); void setFor(int desktop, int screen); public slots: void populateDesktopList(); void accept() override; void buttonClicked(QAbstractButton* button); void browseWallpaper(); void adjustUiForScreen(); void reload(); private: // Gets screen (0-based). int screen() const; // Gets selecged desktop. std::string desktop() const; void loadData(); void saveData(); Ui::WallpaperSettingsDialog *ui; MultiDockModel* model_; DesktopEnv* desktopEnv_; // Path to wallpaper file. QString wallpaper_; // Remember the current directory of the session when opening the file dialog // for browsing wallpapers. QString currentDir_; bool multiScreen_; }; } // namespace crystaldock #endif // CRYSTALDOCK_WALLPAPER_SETTINGS_DIALOG_H_ dangvd-crystal-dock-7a34e96/src/view/wallpaper_settings_dialog.ui000066400000000000000000000074031512233520000252410ustar00rootroot00000000000000 WallpaperSettingsDialog 0 0 900 791 Wallpaper Settings 40 730 821 32 Qt::Horizontal QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok true 40 90 221 36 Browse Image 170 20 91 36 40 30 121 23 Desktop 650 30 111 23 Screen 770 20 91 36 40 180 821 521 QFrame::Panel QFrame::Plain 10 10 800 500 true 40 150 131 23 Preview buttonBox accepted() WallpaperSettingsDialog accept() 248 254 157 274 buttonBox rejected() WallpaperSettingsDialog reject() 316 260 286 274 dangvd-crystal-dock-7a34e96/src/view/wifi_connection_dialog.cc000066400000000000000000000041121512233520000244510ustar00rootroot00000000000000#include "wifi_connection_dialog.h" #include "ui_wifi_connection_dialog.h" #include #include #include #include #include "wifi_manager.h" namespace crystaldock { WifiConnectionDialog::WifiConnectionDialog(QWidget* parent, WifiManager* manager) : QDialog(parent), ui(new Ui::WifiConnectionDialog), manager_(manager) { ui->setupUi(this); ui->connectButton->setIcon(QIcon::fromTheme("network-wireless")); ui->disconnectButton->setIcon(QIcon::fromTheme("network-wireless")); ui->closeButton->setIcon(QIcon::fromTheme("dialog-close")); connect(ui->connectButton, &QPushButton::clicked, this, &WifiConnectionDialog::connectWifi); connect(ui->disconnectButton, &QPushButton::clicked, this, &WifiConnectionDialog::disconnectWifi); } WifiConnectionDialog::~WifiConnectionDialog() { delete ui; } void WifiConnectionDialog::setData(const WifiNetwork& network) { ui->network->setText(network.name); ui->signal->setText(QString::number(network.signal) + "%"); setInUse(network.inUse); } void WifiConnectionDialog::setInUse(bool inUse) { ui->status->setText(inUse ? "Connected" : "Not connected"); ui->passwordLabel->setVisible(!inUse); ui->password->setVisible(!inUse); ui->password->setText(""); ui->connectButton->setEnabled(true); ui->disconnectButton->setEnabled(true); ui->connectButton->setVisible(!inUse); ui->disconnectButton->setVisible(inUse); } void WifiConnectionDialog::setStatus(const QString &status, bool enableButtons) { ui->status->setText(status); if (enableButtons) { ui->connectButton->setEnabled(true); ui->disconnectButton->setEnabled(true); } } void WifiConnectionDialog::connectWifi() { ui->connectButton->setEnabled(false); setStatus("Connecting...", /*enableButtons=*/false); manager_->connectWifi(ui->network->text(), ui->password->text()); } void WifiConnectionDialog::disconnectWifi() { ui->disconnectButton->setEnabled(false); setStatus("Disconnecting...", /*enableButtons=*/false); manager_->disconnectWifi(ui->network->text()); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/wifi_connection_dialog.h000066400000000000000000000013561512233520000243220ustar00rootroot00000000000000#ifndef CRYSTAL_DOCK_WIFI_CONNECTION_DIALOG_H_ #define CRYSTAL_DOCK_WIFI_CONNECTION_DIALOG_H_ #include namespace Ui { class WifiConnectionDialog; } namespace crystaldock { struct WifiNetwork; class WifiManager; class WifiConnectionDialog : public QDialog { Q_OBJECT public: explicit WifiConnectionDialog(QWidget* parent, WifiManager* manager); ~WifiConnectionDialog(); void setData(const WifiNetwork& network); void setInUse(bool inUse); void setStatus(const QString& status, bool enableButtons = true); public slots: void connectWifi(); void disconnectWifi(); private: Ui::WifiConnectionDialog *ui; WifiManager* manager_; }; } // namespace crystaldock #endif // CRYSTAL_DOCK_WIFI_CONNECTION_DIALOG_H_ dangvd-crystal-dock-7a34e96/src/view/wifi_connection_dialog.ui000066400000000000000000000075721512233520000245160ustar00rootroot00000000000000 WifiConnectionDialog 0 0 500 320 Wi-Fi Network 30 180 151 30 Wi-Fi password 200 170 270 42 QLineEdit::EchoMode::Password 30 30 151 28 Network 200 30 270 30 30 130 151 30 Status 30 80 151 30 Signal strength 200 80 270 30 200 130 270 30 30 240 441 46 20 20 20 Connect Disconnect Close closeButton clicked() WifiConnectionDialog close() 390 262 249 159 dangvd-crystal-dock-7a34e96/src/view/wifi_manager.cc000066400000000000000000000160251512233520000224130ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #include "wifi_manager.h" #include #include #include "dock_panel.h" namespace crystaldock { WifiManager::WifiManager(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize) : IconBasedDockItem(parent, model, kLabel, orientation, kIcon, minSize, maxSize), connectionDialog_(parent_, this) { createMenu(); connect(&menu_, &QMenu::triggered, this, &WifiManager::onNetworkSelected); connect(&menu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); connect(&contextMenu_, &QMenu::aboutToHide, this, [this]() { parent_->setShowingPopup(false); }); scanWifiNetworks(); } WifiManager::~WifiManager() { if (process_ && process_->state() != QProcess::NotRunning) { process_->kill(); process_->waitForFinished(1000); } } void WifiManager::mousePressEvent(QMouseEvent* e) { if (e->button() == Qt::LeftButton) { if (commandExists({kCommand}).isEmpty()) { QMessageBox::warning(parent_, "Command not found", QString("Command '") + kCommand + "' not found. This is required by the " + kLabel + " component."); return; } showWifiNetworks(); } else if (e->button() == Qt::RightButton) { showPopupMenu(&contextMenu_); } } void WifiManager::onNetworkSelected(QAction* action) { WifiNetwork network = action->data().value(); connectionDialog_.setData(network); connectionDialog_.show(); } void WifiManager::rescan() { info_.setText("Rescanning Wi-Fi networks..."); parent_->minimize(); QTimer::singleShot(DockPanel::kExecutionDelayMs, [this]{ info_.show(); }); scanWifiNetworks([this]() { info_.setText("Rescanning completed"); }); } void WifiManager::connectWifi(const QString &network, const QString &password) { if (process_ && process_->state() != QProcess::NotRunning) { return; } process_ = new QProcess(parent_); connect(process_, &QProcess::started, [this, network, password]() { process_->write((password + "\n").toStdString().c_str()); connect(process_, QOverload::of(&QProcess::finished), [this, network](int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode == 0) { connectionDialog_.setInUse(true); setLabel("Wi-Fi: Connected to " + network); if (auto result = std::ranges::find(networks_, network, &WifiNetwork::name); result != networks_.end()) { result->inUse = true; updateWifiList(); } } else { connectionDialog_.setStatus("Failed to connect"); } process_->deleteLater(); process_ = nullptr; }); }); process_->start(kCommand, {"dev", "wifi", "connect", network, "--ask"}); } void WifiManager::disconnectWifi(const QString &network) { if (process_ && process_->state() != QProcess::NotRunning) { return; } process_ = new QProcess(parent_); connect(process_, QOverload::of(&QProcess::finished), [this, network](int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode == 0) { connectionDialog_.setInUse(false); setLabel("Wi-Fi: Not connected"); if (auto result = std::ranges::find(networks_, network, &WifiNetwork::name); result != networks_.end()) { result->inUse = false; updateWifiList(); } } process_->deleteLater(); process_ = nullptr; }); process_->start(kCommand, {"connection", "delete", network}); } void WifiManager::scanWifiNetworks(std::function onSuccess) { if (process_ && process_->state() != QProcess::NotRunning) { return; } process_ = new QProcess(parent_); connect(process_, QOverload::of(&QProcess::finished), [this, onSuccess](int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode == 0) { networks_.clear(); bool connected = false; for (const QString network : process_->readAllStandardOutput().split('\n')) { QStringList fields = network.split(':'); if (fields.size() < 3) continue; const QString& name = fields[0]; unsigned int signal = fields[1].toInt(); bool inUse = !fields[2].trimmed().isEmpty(); if (!name.isEmpty() && signal != 0) { WifiNetwork network = { .name = name, .signal = signal, .inUse = inUse }; networks_.push_back(network); if (network.inUse) { setLabel("Wi-Fi: Connected to " + network.name); connected = true; } } } if (!connected) { setLabel("Wi-Fi: Not connected"); } updateWifiList(); if (onSuccess) { onSuccess(); } } process_->deleteLater(); process_ = nullptr; }); process_->start(kCommand, {"--terse", "--fields", "SSID,SIGNAL,IN-USE", "dev", "wifi", "list"}); } void WifiManager::showWifiNetworks() { showPopupMenu(&menu_); } void WifiManager::updateWifiList() { menu_.clear(); for (const auto& network : networks_) { QString label = network.name + (network.inUse ? " (Connected)" : ""); QAction* action = new QAction(label, &menu_); action->setData(QVariant::fromValue(network)); menu_.addAction(action); } } void WifiManager::createMenu() { contextMenu_.addSection(kLabel); rescanAction_ = contextMenu_.addAction( QIcon::fromTheme("network-wireless"), "Rescan Wi-Fi networks"); connect(rescanAction_, &QAction::triggered, this, &WifiManager::rescan); contextMenu_.addSeparator(); parent_->addPanelSettings(&contextMenu_); } } // namespace crystaldock dangvd-crystal-dock-7a34e96/src/view/wifi_manager.h000066400000000000000000000047001512233520000222520ustar00rootroot00000000000000/* * This file is part of Crystal Dock. * Copyright (C) 2025 Viet Dang (dangvd@gmail.com) * * Crystal Dock is free software: you can 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. * * Crystal Dock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Crystal Dock. If not, see . */ #ifndef CRYSTAL_DOCK_WIFI_MANAGER_H_ #define CRYSTAL_DOCK_WIFI_MANAGER_H_ #include "icon_based_dock_item.h" #include #include #include #include #include #include #include #include #include "view/wifi_connection_dialog.h" namespace crystaldock { struct WifiNetwork { QString name; unsigned int signal; bool inUse; }; // A Wifi manager that integrates with nmcli. class WifiManager : public QObject, public IconBasedDockItem { Q_OBJECT public: WifiManager(DockPanel* parent, MultiDockModel* model, Qt::Orientation orientation, int minSize, int maxSize); virtual ~WifiManager(); void mousePressEvent(QMouseEvent* e) override; bool beforeTask(const QString& program) override { return false; } void connectWifi(const QString& network, const QString& password); void disconnectWifi(const QString& network); public slots: void onNetworkSelected(QAction* action); void rescan(); private: static constexpr char kCommand[] = "nmcli"; static constexpr char kLabel[] = "Wi-Fi Manager"; static constexpr char kIcon[] = "network-wireless"; void scanWifiNetworks(std::function onSuccess = nullptr); void showWifiNetworks(); void updateWifiList(); // Creates the context menu. void createMenu(); std::vector networks_; // nmcli process. QProcess* process_ = nullptr; // Left-click volume menu. QMenu menu_; // Right-click context menu. QMenu contextMenu_; QAction* rescanAction_; WifiConnectionDialog connectionDialog_; QMessageBox info_; }; } // namespace crystaldock Q_DECLARE_METATYPE(crystaldock::WifiNetwork); #endif // CRYSTAL_DOCK_WIFI_MANAGER_H_