commit a2b98f661de9234704e5260e52929bf1971d69db Author: cutefishd Date: Tue Mar 16 11:17:11 2021 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..733eeec --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# C++ objects and libs +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.so.* +*.dll +*.dylib + +# Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +*.qmlc +*.jsc +Makefile* +*build-* +*.qm +*.prl + +# Qt unit tests +target_wrapper.* + +# QtCreator +*.autosave + +# QtCreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCreator CMake +CMakeLists.txt.user* + +# QtCreator 4.8< compilation database +compile_commands.json + +# QtCreator local machine specific files for imported projects +*creator.user* + +build/* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..262494c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,74 @@ +cmake_minimum_required(VERSION 3.5) + +set(PROJECT_NAME cyber-dock) +project(${PROJECT_NAME}) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(QT Core Widgets Concurrent Quick QuickControls2 X11Extras DBus LinguistTools) +find_package(Qt5 REQUIRED ${QT}) +find_package(KF5WindowSystem REQUIRED) +find_package(dbusmenu-qt5 REQUIRED) +find_package(MeuiKit REQUIRED) + +set(SRCS + src/applicationitem.h + src/applicationmodel.cpp + src/battery.cpp + src/brightness.cpp + src/controlcenterdialog.cpp + src/docksettings.cpp + src/iconthemeimageprovider.cpp + src/main.cpp + src/mainwindow.cpp + src/systemappmonitor.cpp + src/systemappitem.cpp + src/processprovider.cpp + src/trashmanager.cpp + src/volumemanager.cpp + src/utils.cpp + src/xwindowinterface.cpp + + src/appearance.cpp + src/fakewindow.cpp + + src/statusnotifier/dbustypes.cpp + src/statusnotifier/sniasync.cpp + src/statusnotifier/statusnotifieriteminterface.cpp + src/statusnotifier/statusnotifiermodel.cpp + src/statusnotifier/statusnotifierwatcher.cpp + src/statusnotifier/statusnotifieritemsource.cpp +) + +set(RESOURCES + resources.qrc +) + +add_executable(${PROJECT_NAME} ${SRCS} ${DBUS_SRCS} ${RESOURCES}) +target_link_libraries(${PROJECT_NAME} + Qt5::Core + Qt5::Widgets + Qt5::Quick + Qt5::QuickControls2 + Qt5::X11Extras + Qt5::Concurrent + Qt5::DBus + + MeuiKit + + KF5::WindowSystem + dbusmenu-qt5 +) + +file(GLOB TS_FILES translations/*.ts) +qt5_create_translation(QM_FILES ${TS_FILES}) +add_custom_target(translations DEPENDS ${QM_FILES} SOURCES ${TS_FILES}) +add_dependencies(${PROJECT_NAME} translations) + +install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION /usr/bin) +install(FILES ${QM_FILES} DESTINATION /usr/share/${PROJECT_NAME}/translations) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..df9481e --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Dock + +CutefishOS application dock. + +## Dependencies + +```shell +sudo pacman -S gcc cmake qt5-base qt5-quickcontrols2 kwindowsystem +``` + +You also need [`meuikit`](https://github.com/cyberos/meuikit) and [`libcyber-system`](https://github.com/cyberos/libcyber-system). + +## Build and Install + +``` +mkdir build +cd build +cmake .. +make +sudo make install +``` + +## License + +This project has been licensed by GPLv3. diff --git a/qml/AppItem.qml b/qml/AppItem.qml new file mode 100644 index 0000000..6363df0 --- /dev/null +++ b/qml/AppItem.qml @@ -0,0 +1,75 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import Qt.labs.platform 1.0 +import Cyber.Dock 1.0 + +DockItem { + id: appItem + + property var windowCount: model.windowCount + + iconName: model.iconName ? model.iconName : "application-x-desktop" + isActive: model.isActive + popupText: model.visibleName + enableActivateDot: windowCount !== 0 + draggable: model.appId === "cyber-launcher" ? false : true + dragItemIndex: index + + onWindowCountChanged: { + if (windowCount > 0) + updateGeometry() + } + + onPositionChanged: updateGeometry() + onPressed: updateGeometry() + onClicked: appModel.clicked(model.appId) + onRightClicked: if (model.appId !== "cyber-launcher") contextMenu.open() + + dropArea.onEntered: { + if (drag.source) + appModel.move(drag.source.dragItemIndex, appItem.dragItemIndex) + else + appModel.raiseWindow(model.appId) + } + + dropArea.onDropped: { + appModel.save() + updateGeometry() + } + + Menu { + id: contextMenu + + MenuItem { + text: qsTr("Open") + visible: windowCount === 0 + onTriggered: appModel.openNewInstance(model.appId) + } + + MenuItem { + text: model.visibleName + visible: windowCount > 0 + onTriggered: appModel.openNewInstance(model.appId) + } + + MenuItem { + text: model.isPinned ? qsTr("Unpin") : qsTr("Pin") + onTriggered: { + model.isPinned ? appModel.unPin(model.appId) : appModel.pin(model.appId) + } + } + + MenuItem { + text: qsTr("Close All") + visible: windowCount !== 0 + onTriggered: appModel.closeAllByAppId(model.appId) + } + } + + + function updateGeometry() { + appModel.updateGeometries(model.appId, Qt.rect(appItem.mapToGlobal(0, 0).x, + appItem.mapToGlobal(0, 0).y, + appItem.width, appItem.height)) + } +} diff --git a/qml/CardItem.qml b/qml/CardItem.qml new file mode 100644 index 0000000..e747b1b --- /dev/null +++ b/qml/CardItem.qml @@ -0,0 +1,120 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtGraphicalEffects 1.0 +import MeuiKit 1.0 as Meui + +Item { + id: control + + property bool checked: false + property alias icon: _image.source + property alias label: _titleLabel.text + property alias text: _label.text + + signal clicked + + property var hoverColor: Meui.Theme.darkMode ? Qt.lighter(Meui.Theme.secondBackgroundColor, 2) + : Qt.darker(Meui.Theme.secondBackgroundColor, 1.3) + property var pressedColor: Meui.Theme.darkMode ? Qt.lighter(Meui.Theme.secondBackgroundColor, 1.8) + : Qt.darker(Meui.Theme.secondBackgroundColor, 1.5) + + property var highlightHoverColor: Meui.Theme.darkMode ? Qt.lighter(Meui.Theme.highlightColor, 1.2) + : Qt.darker(Meui.Theme.highlightColor, 1.1) + property var highlightPressedColor: Meui.Theme.darkMode ? Qt.lighter(Meui.Theme.highlightColor, 1.1) + : Qt.darker(Meui.Theme.highlightColor, 1.2) + + MouseArea { + id: _mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onClicked: control.clicked() + + onPressedChanged: { + control.scale = pressed ? 0.95 : 1.0 + } + } + + Behavior on scale { + NumberAnimation { + duration: 100 + } + } + + Meui.RoundedRect { + anchors.fill: parent + radius: Meui.Theme.bigRadius + backgroundOpacity: control.checked ? 0.9 : 0.3 + animationEnabled: false + + color: { + if (control.checked) { + if (_mouseArea.pressed) + return highlightPressedColor + else if (_mouseArea.containsMouse) + return highlightHoverColor + else + return Meui.Theme.highlightColor + } else { + if (_mouseArea.pressed) + return pressedColor + else if (_mouseArea.containsMouse) + return hoverColor + else + return Meui.Theme.secondBackgroundColor + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.leftMargin: Meui.Units.smallSpacing + anchors.rightMargin: Meui.Units.smallSpacing + + Image { + id: _image + Layout.preferredWidth: control.height * 0.3 + Layout.preferredHeight: control.height * 0.3 + sourceSize: Qt.size(width, height) + asynchronous: true + Layout.alignment: Qt.AlignCenter + Layout.topMargin: Meui.Units.largeSpacing + + ColorOverlay { + anchors.fill: _image + source: _image + color: control.checked ? Meui.Theme.highlightedTextColor : Meui.Theme.disabledTextColor + } + } + + Item { + Layout.fillHeight: true + } + + Label { + id: _titleLabel + color: control.checked ? Meui.Theme.highlightedTextColor : Meui.Theme.textColor + Layout.preferredHeight: control.height * 0.15 + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + + Label { + id: _label + color: control.checked ? Meui.Theme.highlightedTextColor : Meui.Theme.textColor + elide: Label.ElideRight + Layout.preferredHeight: control.height * 0.1 + Layout.alignment: Qt.AlignHCenter + // Layout.fillWidth: true + Layout.bottomMargin: Meui.Units.largeSpacing + } + + Item { + Layout.fillHeight: true + } + } +} diff --git a/qml/ControlCenter.qml b/qml/ControlCenter.qml new file mode 100644 index 0000000..a2a4f9f --- /dev/null +++ b/qml/ControlCenter.qml @@ -0,0 +1,352 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Window 2.12 +import QtQuick.Layouts 1.12 +import QtGraphicalEffects 1.0 + +import Cyber.Dock 1.0 +import MeuiKit 1.0 as Meui +import Cyber.Accounts 1.0 as Accounts + +ControlCenterDialog { + id: control + width: 500 + height: _mainLayout.implicitHeight + Meui.Units.largeSpacing * 4 + + minimumWidth: 500 + maximumWidth: 500 + minimumHeight: _mainLayout.implicitHeight + Meui.Units.largeSpacing * 4 + maximumHeight: _mainLayout.implicitHeight + Meui.Units.largeSpacing * 4 + + property point position: Qt.point(0, 0) + + onWidthChanged: adjustCorrectLocation() + onHeightChanged: adjustCorrectLocation() + onPositionChanged: adjustCorrectLocation() + + color: "transparent" + + Appearance { + id: appearance + } + + function adjustCorrectLocation() { + var posX = control.position.x + var posY = control.position.y + + // left + if (posX < 0) + posX = Meui.Units.largeSpacing + + // top + if (posY < 0) + posY = Meui.Units.largeSpacing + + // right + if (posX + control.width > Screen.width) + posX = Screen.width - control.width - Meui.Units.largeSpacing + + // bottom + if (posY > control.height > Screen.width) + posY = Screen.width - control.width - Meui.Units.largeSpacing + + control.x = posX + control.y = posY + } + + Brightness { + id: brightness + } + + Accounts.UserAccount { + id: currentUser + } + + Meui.WindowBlur { + view: control + geometry: Qt.rect(control.x, control.y, control.width, control.height) + windowRadius: _background.radius + enabled: true + } + + Meui.RoundedRect { + id: _background + anchors.fill: parent + radius: control.height * 0.05 + color: Meui.Theme.backgroundColor + backgroundOpacity: Meui.Theme.darkMode ? 0.3 : 0.4 + } + + Meui.WindowShadow { + view: control + geometry: Qt.rect(control.x, control.y, control.width, control.height) + radius: _background.radius + } + + ColumnLayout { + id: _mainLayout + anchors.fill: parent + anchors.margins: Meui.Units.largeSpacing * 2 + spacing: Meui.Units.largeSpacing + + Item { + id: topItem + Layout.fillWidth: true + height: 50 + + RowLayout { + id: topItemLayout + anchors.fill: parent + spacing: Meui.Units.largeSpacing + + Image { + id: userIcon + Layout.fillHeight: true + width: height + sourceSize: Qt.size(width, height) + source: currentUser.iconFileName ? "file:///" + currentUser.iconFileName : "image://icontheme/default-user" + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Item { + width: userIcon.width + height: userIcon.height + + Rectangle { + anchors.fill: parent + radius: parent.height / 2 + } + } + } + } + + Label { + id: userLabel + text: currentUser.userName + Layout.fillHeight: true + Layout.fillWidth: true + elide: Label.ElideRight + } + + IconButton { + id: settingsButton + implicitWidth: topItem.height * 0.8 + implicitHeight: topItem.height * 0.8 + Layout.alignment: Qt.AlignTop + source: "qrc:/svg/" + (Meui.Theme.darkMode ? "dark/" : "light/") + "settings.svg" + onLeftButtonClicked: { + control.visible = false + process.startDetached("cyber-settings") + } + } + + IconButton { + id: shutdownButton + implicitWidth: topItem.height * 0.8 + implicitHeight: topItem.height * 0.8 + Layout.alignment: Qt.AlignTop + source: "qrc:/svg/" + (Meui.Theme.darkMode ? "dark/" : "light/") + "system-shutdown-symbolic.svg" + onLeftButtonClicked: { + control.visible = false + process.startDetached("cyber-shutdown") + } + } + } + } + + Item { + id: controlItem + Layout.fillWidth: true + height: 120 + visible: wirelessItem.visible || bluetoothItem.visible + + RowLayout { + anchors.fill: parent + spacing: Meui.Units.largeSpacing + + CardItem { + id: wirelessItem + Layout.fillHeight: true + Layout.preferredWidth: contentItem.width / 3 - Meui.Units.largeSpacing * 2 + icon: "qrc:/svg/dark/network-wireless-connected-100.svg" + visible: network.wirelessHardwareEnabled + checked: network.wirelessEnabled + label: qsTr("Wi-Fi") + text: network.wirelessEnabled ? network.wirelessConnectionName ? + network.wirelessConnectionName : + qsTr("On") : qsTr("Off") + onClicked: network.wirelessEnabled = !network.wirelessEnabled + } + + CardItem { + id: bluetoothItem + Layout.fillHeight: true + Layout.preferredWidth: contentItem.width / 3 - Meui.Units.largeSpacing * 2 + icon: "qrc:/svg/light/bluetooth-symbolic.svg" + checked: false + label: qsTr("Bluetooth") + text: qsTr("Off") + } + + CardItem { + id: darkModeItem + Layout.fillHeight: true + Layout.preferredWidth: contentItem.width / 3 - Meui.Units.largeSpacing * 2 + icon: "qrc:/svg/light/dark-mode.svg" + checked: Meui.Theme.darkMode + label: qsTr("Dark Mode") + text: Meui.Theme.darkMode ? qsTr("On") : qsTr("Off") + onClicked: appearance.switchDarkMode(!Meui.Theme.darkMode) + } + + Item { + Layout.fillWidth: true + } + } + } + + MprisController { + height: 100 + Layout.fillWidth: true + } + + Item { + id: brightnessItem + Layout.fillWidth: true + height: 50 + visible: brightness.enabled + + Meui.RoundedRect { + id: brightnessItemBg + anchors.fill: parent + anchors.margins: 0 + radius: Meui.Theme.bigRadius + color: Meui.Theme.backgroundColor + backgroundOpacity: 0.3 + } + + RowLayout { + anchors.fill: brightnessItemBg + anchors.margins: Meui.Units.largeSpacing + spacing: Meui.Units.largeSpacing + + Image { + width: parent.height * 0.6 + height: parent.height * 0.6 + sourceSize: Qt.size(width, height) + source: "qrc:/svg/" + (Meui.Theme.darkMode ? "dark" : "light") + "/brightness.svg" + } + + Slider { + id: brightnessSlider + from: 0 + to: 100 + stepSize: 1 + value: brightness.value + Layout.fillWidth: true + Layout.fillHeight: true + + onMoved: { + brightness.setValue(brightnessSlider.value) + } + } + } + } + + Item { + id: volumeItem + Layout.fillWidth: true + height: 50 + visible: volume.isValid + + Meui.RoundedRect { + id: volumeItemBg + anchors.fill: parent + anchors.margins: 0 + radius: Meui.Theme.bigRadius + color: Meui.Theme.backgroundColor + backgroundOpacity: 0.3 + } + + RowLayout { + anchors.fill: volumeItemBg + anchors.margins: Meui.Units.largeSpacing + spacing: Meui.Units.largeSpacing + + Image { + width: parent.height * 0.6 + height: parent.height * 0.6 + sourceSize: Qt.size(width, height) + source: "qrc:/svg/" + (Meui.Theme.darkMode ? "dark" : "light") + "/" + volume.iconName + ".svg" + } + + Slider { + id: slider + from: 0 + to: 100 + stepSize: 1 + value: volume.volume + Layout.fillWidth: true + Layout.fillHeight: true + + onValueChanged: { + volume.setVolume(value) + + if (volume.isMute && value > 0) + volume.setMute(false) + } + } + } + } + + RowLayout { + Label { + id: timeLabel + + Timer { + interval: 1000 + repeat: true + running: true + triggeredOnStart: true + onTriggered: { + timeLabel.text = new Date().toLocaleString(Qt.locale(), Locale.ShortFormat) + } + } + } + + Item { + Layout.fillWidth: true + } + + StandardItem { + width: batteryLayout.implicitWidth + Meui.Units.largeSpacing + height: batteryLayout.implicitHeight + Meui.Units.largeSpacing + + onClicked: process.startDetached("cyber-settings", ["-m", "battery"]) + + RowLayout { + id: batteryLayout + anchors.fill: parent + visible: battery.available + spacing: 0 + + Image { + id: batteryIcon + width: 22 + height: 16 + sourceSize: Qt.size(width, height) + source: "qrc:/svg/" + (Meui.Theme.darkMode ? "dark/" : "light/") + battery.iconSource + asynchronous: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + } + + Label { + text: battery.chargePercent + "%" + color: Meui.Theme.textColor + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + } + } + } + } + } +} diff --git a/qml/ControlCenterItem.qml b/qml/ControlCenterItem.qml new file mode 100644 index 0000000..bb045bd --- /dev/null +++ b/qml/ControlCenterItem.qml @@ -0,0 +1,93 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtGraphicalEffects 1.0 + +import Cyber.Dock 1.0 +import MeuiKit 1.0 as Meui + +StandardItem { + id: controlItem + + Layout.preferredWidth: isHorizontal ? controlLayout.implicitWidth + Meui.Units.largeSpacing * 2 : mainLayout.width * 0.7 + Layout.preferredHeight: isHorizontal ? mainLayout.height * 0.7 : controlLayout.implicitHeight + Meui.Units.largeSpacing * 2 + Layout.rightMargin: isHorizontal ? root.windowRadius / 2 : 0 + Layout.bottomMargin: isHorizontal ? 0 : root.windowRadius / 2 + Layout.alignment: Qt.AlignCenter + + onClicked: { + if (controlCenter.visible) + controlCenter.visible = false + else { + // 先初始化,用户可能会通过Alt鼠标左键移动位置 + controlCenter.position = Qt.point(0, 0) + + controlCenter.visible = true + controlCenter.position = Qt.point(mapToGlobal(0, 0).x, mapToGlobal(0, 0).y) + } + } + + GridLayout { + id: controlLayout + anchors.fill: parent + anchors.leftMargin: isHorizontal ? Meui.Units.smallSpacing : 0 + anchors.rightMargin: isHorizontal ? Meui.Units.smallSpacing : 0 + anchors.topMargin: isHorizontal ? Meui.Units.smallSpacing : 0 + anchors.bottomMargin: isHorizontal ? Meui.Units.smallSpacing : 0 + columnSpacing: isHorizontal ? Meui.Units.largeSpacing + Meui.Units.smallSpacing : 0 + rowSpacing: isHorizontal ? 0 : Meui.Units.largeSpacing + Meui.Units.smallSpacing + flow: isHorizontal ? Grid.LeftToRight : Grid.TopToBottom + + Image { + id: wirelessIcon + width: root.trayItemSize + height: width + sourceSize: Qt.size(width, height) + source: network.wirelessIconName ? "qrc:/svg/" + (Meui.Theme.darkMode ? "dark/" : "light/") + network.wirelessIconName + ".svg" : "" + asynchronous: true + Layout.alignment: Qt.AlignCenter + visible: network.enabled && + network.wirelessEnabled && + network.wirelessConnectionName !== "" && + wirelessIcon.status === Image.Ready + } + + Image { + id: batteryIcon + visible: battery.available && status === Image.Ready + height: root.trayItemSize + width: height + 6 + sourceSize: Qt.size(width, height) + source: "qrc:/svg/" + (Meui.Theme.darkMode ? "dark/" : "light/") + battery.iconSource + asynchronous: true + Layout.alignment: Qt.AlignCenter + } + + Image { + id: volumeIcon + visible: volume.isValid && status === Image.Ready + source: "qrc:/svg/" + (Meui.Theme.darkMode ? "dark/" : "light/") + volume.iconName + ".svg" + width: root.trayItemSize + height: width + sourceSize: Qt.size(width, height) + asynchronous: true + Layout.alignment: Qt.AlignCenter + } + + Label { + id: timeLabel + Layout.alignment: Qt.AlignCenter + font.pixelSize: isHorizontal ? controlLayout.height / 3 : controlLayout.width / 5 + + Timer { + interval: 1000 + repeat: true + running: true + triggeredOnStart: true + onTriggered: { + timeLabel.text = new Date().toLocaleTimeString(Qt.locale(), Locale.ShortFormat) + } + } + } + } +} diff --git a/qml/DockItem.qml b/qml/DockItem.qml new file mode 100644 index 0000000..baf7720 --- /dev/null +++ b/qml/DockItem.qml @@ -0,0 +1,175 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.0 +import Cyber.Dock 1.0 +import MeuiKit 1.0 as Meui + +Item { + id: control + + property bool isLeft: Settings.direction === DockSettings.Left + + property var iconSize: (isLeft ? control.height : control.width) * iconSizeRatio + + property bool draggable: false + property int dragItemIndex + + property alias icon: icon + property alias mouseArea: iconArea + property alias dropArea: iconDropArea + + property bool enableActivateDot: true + property bool isActive: false + + property var popupText + + property double iconSizeRatio: 0.8 + property var iconName + + property bool dragStarted: false + + signal positionChanged() + signal released() + signal pressed(var mouse) + signal pressAndHold(var mouse) + signal clicked(var mouse) + signal rightClicked(var mouse) + signal doubleClicked(var mouse) + + Drag.active: mouseArea.drag.active && control.draggable + Drag.dragType: Drag.Automatic + Drag.supportedActions: Qt.MoveAction + Drag.hotSpot.x: icon.width / 2 + Drag.hotSpot.y: icon.height / 2 + + Drag.onDragStarted: { + dragStarted = true + } + + Drag.onDragFinished: { + dragStarted = false + } + + Image { + id: icon + anchors.centerIn: parent + source: iconName ? iconName.indexOf("/") === 0 || iconName.indexOf("file://") === 0 || iconName.indexOf("qrc") === 0 + ? iconName : "image://icontheme/" + iconName : iconName + sourceSize.width: control.iconSize + sourceSize.height: control.iconSize + width: sourceSize.width + height: sourceSize.height + asynchronous: false + smooth: true + cache: true + + visible: !dragStarted + + ColorOverlay { + id: iconColorize + anchors.fill: icon + source: icon + color: "#000000" + opacity: iconArea.pressed && !mouseArea.drag.active ? 0.5 : 0 + } + } + + DropArea { + id: iconDropArea + anchors.fill: icon + enabled: draggable + } + + MouseArea { + id: iconArea + anchors.fill: icon + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + drag.axis: Drag.XAndYAxis + + onClicked: { + if (mouse.button === Qt.RightButton) + control.rightClicked(mouse) + else + control.clicked(mouse) + } + + onPressed: { + control.pressed(mouse) + popupTips.hide() + } + + onPositionChanged: { + if (pressed) { + if (mouse.source !== Qt.MouseEventSynthesizedByQt) { + drag.target = icon + icon.grabToImage(function(result) { + control.Drag.imageSource = result.url + }) + } else { + drag.target = null + } + } + + control.positionChanged() + } + + onPressAndHold : control.pressAndHold(mouse) + onReleased: { + drag.target = null + control.released() + } + + onContainsMouseChanged: { + if (containsMouse && control.popupText !== "") { + popupTips.popupText = control.popupText + + if (Settings.direction === DockSettings.Left) + popupTips.position = Qt.point(root.width + Meui.Units.largeSpacing, + control.mapToGlobal(0, 0).y + (control.height / 2 - popupTips.height / 2)) + else + popupTips.position = Qt.point(control.mapToGlobal(0, 0).x + (control.width / 2 - popupTips.width / 2), + control.mapToGlobal(0, 0).y - popupTips.height - Meui.Units.smallSpacing / 2) + + popupTips.show() + } else { + popupTips.hide() + } + } + } + + Rectangle { + id: activeLine + width: isLeft ? parent.width * 0.06 : (isActive ? parent.height * 0.4 : parent.height * 0.06) + height: isLeft ? (isActive ? parent.height * 0.4 : parent.height * 0.06) : parent.height * 0.06 + color: Meui.Theme.textColor + radius: isLeft ? width / 2 : height / 2 + visible: enableActivateDot && !dragStarted + opacity: isActive ? 1 : 0.6 + + Behavior on opacity { + NumberAnimation { + duration: 125 + easing.type: Easing.InOutCubic + } + } + + Behavior on width { + NumberAnimation { + duration: !isLeft ? 125 : 0 + easing.type: Easing.InOutCubic + } + } + + Behavior on height { + NumberAnimation { + duration: isLeft ? 125 : 0 + easing.type: Easing.InOutCubic + } + } + + x: isLeft ? 3 : (parent.width - width) / 2 + y: isLeft ? (parent.height - height) / 2 : icon.y + icon.height + activeLine.height / 2 - 2 + // 1 is the window border + } +} diff --git a/qml/IconButton.qml b/qml/IconButton.qml new file mode 100644 index 0000000..06c6dc9 --- /dev/null +++ b/qml/IconButton.qml @@ -0,0 +1,57 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import MeuiKit 1.0 as Meui + +Item { + id: control + + property url source + property real size: 24 + property string popupText + + signal leftButtonClicked + signal rightButtonClicked + + MouseArea { + id: mouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: control.visible ? true : false + + onClicked: { + if (mouse.button === Qt.LeftButton) + control.leftButtonClicked() + else if (mouse.button === Qt.RightButton) + control.rightButtonClicked() + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: parent.height * 0.2 + + color: { + if (mouseArea.containsMouse) { + if (mouseArea.containsPress) + return (Meui.Theme.darkMode) ? Qt.rgba(255, 255, 255, 0.3) : Qt.rgba(0, 0, 0, 0.3) + else + return (Meui.Theme.darkMode) ? Qt.rgba(255, 255, 255, 0.2) : Qt.rgba(0, 0, 0, 0.2) + } + + return "transparent" + } + } + + Image { + id: iconImage + anchors.centerIn: parent + width: parent.height * 0.8 + height: width + sourceSize.width: width + sourceSize.height: height + source: control.source + asynchronous: true + } +} diff --git a/qml/MprisController.qml b/qml/MprisController.qml new file mode 100644 index 0000000..2cfe19e --- /dev/null +++ b/qml/MprisController.qml @@ -0,0 +1,137 @@ +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.0 +import MeuiKit 1.0 as Meui +import Cyber.Mpris 1.0 + +Item { + id: control + + visible: available && (_songLabel.text != "" || _artistLabel.text != "") + + property bool available: mprisManager.availableServices.length > 1 + property bool isPlaying: currentService && mprisManager.playbackStatus === Mpris.Playing + property alias currentService: mprisManager.currentService + property var artUrlTag: Mpris.metadataToString(Mpris.ArtUrl) + property var titleTag: Mpris.metadataToString(Mpris.Title) + property var artistTag: Mpris.metadataToString(Mpris.Artist) + + MprisManager { + id: mprisManager + } + + Meui.RoundedRect { + id: _background + anchors.fill: parent + anchors.margins: 0 + radius: Meui.Theme.bigRadius + color: Meui.Theme.backgroundColor + backgroundOpacity: 0.3 + } + + RowLayout { + id: _mainLayout + anchors.fill: parent + anchors.margins: Meui.Units.largeSpacing + anchors.rightMargin: Meui.Units.largeSpacing * 2 + spacing: Meui.Units.largeSpacing + + Image { + id: artImage + Layout.fillHeight: true + width: height + visible: status === Image.Ready + sourceSize: Qt.size(width, height) + source: control.available ? (artUrlTag in mprisManager.metadata) ? mprisManager.metadata[artUrlTag].toString() : "" : "" + asynchronous: true + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Item { + width: artImage.width + height: artImage.height + + Rectangle { + anchors.fill: parent + radius: Meui.Theme.bigRadius + } + } + } + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + + ColumnLayout { + anchors.fill: parent + + Item { + Layout.fillHeight: true + } + + Label { + id: _songLabel + Layout.fillWidth: true + visible: _songLabel.text !== "" + text: control.available ? (titleTag in mprisManager.metadata) ? mprisManager.metadata[titleTag].toString() : "" : "" + elide: Text.ElideRight + } + + Label { + id: _artistLabel + Layout.fillWidth: true + visible: _artistLabel.text !== "" + text: control.available ? (artistTag in mprisManager.metadata) ? mprisManager.metadata[artistTag].toString() : "" : "" + elide: Text.ElideRight + } + + Item { + Layout.fillHeight: true + } + } + } + + Item { + id: _buttons + Layout.fillHeight: true + Layout.preferredWidth: _mainLayout.width / 3 + + RowLayout { + anchors.fill: parent + + IconButton { + width: 33 + height: 33 + source: "qrc:/svg/" + (Meui.Theme.darkMode ? "dark" : "light") + "/media-skip-backward-symbolic.svg" + onLeftButtonClicked: if (mprisManager.canGoPrevious) mprisManager.previous() + visible: control.available ? mprisManager.canGoPrevious : false + Layout.alignment: Qt.AlignRight + } + + IconButton { + width: 33 + height: 33 + source: control.isPlaying ? "qrc:/svg/" + (Meui.Theme.darkMode ? "dark" : "light") + "/media-playback-pause-symbolic.svg" + : "qrc:/svg/" + (Meui.Theme.darkMode ? "dark" : "light") + "/media-playback-start-symbolic.svg" + Layout.alignment: Qt.AlignRight + visible: mprisManager.canPause || mprisManager.canPlay + onLeftButtonClicked: + if ((control.isPlaying && mprisManager.canPause) || (!control.isPlaying && mprisManager.canPlay)) { + mprisManager.playPause() + } + } + + IconButton { + width: 33 + height: 33 + source: "qrc:/svg/" + (Meui.Theme.darkMode ? "dark" : "light") + "/media-skip-forward-symbolic.svg" + Layout.alignment: Qt.AlignRight + onLeftButtonClicked: if (mprisManager.canGoNext) mprisManager.next() + visible: control.available ? mprisManager.canGoNext : false + } + } + } + } +} diff --git a/qml/StandardItem.qml b/qml/StandardItem.qml new file mode 100644 index 0000000..4b394e6 --- /dev/null +++ b/qml/StandardItem.qml @@ -0,0 +1,74 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtGraphicalEffects 1.0 + +import MeuiKit 1.0 as Meui +import Cyber.Dock 1.0 + +Item { + id: control + + property var popupText: "" + + signal clicked + signal rightClicked + + MouseArea { + id: _mouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + + onClicked: { + if (mouse.button == Qt.LeftButton) + control.clicked() + else if (mouse.button == Qt.RightButton) + control.rightClicked() + } + + onPressed: { + popupTips.hide() + } + + onContainsMouseChanged: { + if (containsMouse && control.popupText !== "") { + popupTips.popupText = control.popupText + + if (Settings.direction === DockSettings.Left) + popupTips.position = Qt.point(root.width + Meui.Units.largeSpacing, + control.mapToGlobal(0, 0).y + (control.height / 2 - popupTips.height / 2)) + else + popupTips.position = Qt.point(control.mapToGlobal(0, 0).x + (control.width / 2 - popupTips.width / 2), + mainWindow.y - popupTips.height - Meui.Units.smallSpacing / 2) + + popupTips.show() + } else { + popupTips.hide() + } + } + } + + Rectangle { + anchors.fill: parent + radius: Meui.Theme.smallRadius + + color: { + if (_mouseArea.containsMouse) { + if (_mouseArea.containsPress) + return (Meui.Theme.darkMode) ? Qt.rgba(255, 255, 255, 0.3) : Qt.rgba(0, 0, 0, 0.2) + else + return (Meui.Theme.darkMode) ? Qt.rgba(255, 255, 255, 0.2) : Qt.rgba(0, 0, 0, 0.1) + } + + return "transparent" + } + + Behavior on color { + ColorAnimation { + duration: 125 + easing.type: Easing.InOutCubic + } + } + } +} diff --git a/qml/main.qml b/qml/main.qml new file mode 100644 index 0000000..ad667d2 --- /dev/null +++ b/qml/main.qml @@ -0,0 +1,260 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtGraphicalEffects 1.0 + +import Cyber.NetworkManagement 1.0 as NM +import Cyber.Dock 1.0 +import MeuiKit 1.0 as Meui + +Item { + id: root + visible: true + + property color borderColor: Meui.Theme.darkMode ? Qt.rgba(255, 255, 255, 0.1) : Qt.rgba(0, 0, 0, 0.05) + property real windowRadius: (Settings.direction === DockSettings.Left) ? root.width * 0.25 : root.height * 0.25 + property bool isHorizontal: Settings.direction !== DockSettings.Left + property var appViewLength: isHorizontal ? appItemView.width : appItemView.height + property var appViewHeight: isHorizontal ? appItemView.height : appItemView.width + property var trayItemSize: 16 + property var iconSize: 0 + + DropArea { + anchors.fill: parent + enabled: true + } + + Timer { + id: calcIconSizeTimer + repeat: false + running: false + interval: 10 + onTriggered: calcIconSize() + } + + function calcIconSize() { + const appCount = appItemView.count + const size = (appViewLength - (appViewLength % appCount)) / appCount + const rootHeight = isHorizontal ? root.height : root.width + const calcSize = size >= rootHeight ? rootHeight : Math.min(size, rootHeight) + root.iconSize = calcSize + } + + function delayCalcIconSize() { + if (calcIconSizeTimer.running) + calcIconSizeTimer.stop() + + calcIconSizeTimer.interval = 100 + calcIconSizeTimer.restart() + } + + Meui.WindowShadow { + view: mainWindow + geometry: Qt.rect(root.x, root.y, root.width, root.height) + radius: _background.radius + } + + Meui.WindowBlur { + view: mainWindow + geometry: Qt.rect(root.x, root.y, root.width, root.height) + windowRadius: _background.radius + enabled: true + } + + // Background + Rectangle { + id: _background + anchors.fill: parent + radius: windowRadius + color: Meui.Theme.backgroundColor + opacity: Meui.Theme.darkMode ? 0.3 : 0.5 + + Behavior on color { + ColorAnimation { + duration: 0 + easing.type: Easing.InOutQuad + } + } + } + + Rectangle { + anchors.fill: parent + color: "transparent" + radius: windowRadius + border.width: 1 + border.color: Qt.rgba(0, 0, 0, 0.5) + antialiasing: true + smooth: true + } + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: windowRadius - 1 + color: "transparent" + border.width: 1 + border.color: Qt.rgba(255, 255, 255, 0.2) + antialiasing: true + smooth: true + } + + Meui.PopupTips { + id: popupTips + backgroundColor: Meui.Theme.backgroundColor + backgroundOpacity: Meui.Theme.darkMode ? 0.3 : 0.4 + } + + GridLayout { + id: mainLayout + anchors.fill: parent + flow: isHorizontal ? Grid.LeftToRight : Grid.TopToBottom + columnSpacing: 0 + rowSpacing: 0 + + ListView { + id: appItemView + orientation: isHorizontal ? Qt.Horizontal : Qt.Vertical + snapMode: ListView.SnapToItem + interactive: false + model: appModel + clip: true + + Layout.fillHeight: true + Layout.fillWidth: true + + onCountChanged: root.delayCalcIconSize() + + delegate: AppItem { + id: appItemDelegate + implicitWidth: isHorizontal ? root.iconSize : ListView.view.width + implicitHeight: isHorizontal ? ListView.view.height : root.iconSize + +// Behavior on implicitWidth { +// NumberAnimation { +// from: root.iconSize +// easing.type: Easing.InOutQuad +// duration: isHorizontal ? 250 : 0 +// } +// } + +// Behavior on implicitHeight { +// NumberAnimation { +// from: root.iconSize +// easing.type: Easing.InOutQuad +// duration: !isHorizontal ? 250 : 0 +// } +// } + } + + moveDisplaced: Transition { + NumberAnimation { + properties: "x, y" + duration: 300 + easing.type: Easing.InOutQuad + } + } + } + + ListView { + id: trayView + + property var itemWidth: isHorizontal ? root.trayItemSize + Meui.Units.largeSpacing * 2 : mainLayout.width * 0.7 + property var itemHeight: isHorizontal ? mainLayout.height * 0.7 : root.trayItemSize + Meui.Units.largeSpacing * 2 + + Layout.preferredWidth: isHorizontal ? itemWidth * count + count * trayView.spacing : mainLayout.width * 0.7 + Layout.preferredHeight: isHorizontal ? mainLayout.height * 0.7 : itemHeight * count + count * trayView.spacing + Layout.alignment: Qt.AlignCenter + + orientation: isHorizontal ? Qt.Horizontal : Qt.Vertical + layoutDirection: Qt.RightToLeft + interactive: false + model: trayModel + spacing: Meui.Units.smallSpacing + clip: true + + StatusNotifierModel { + id: trayModel + } + + onCountChanged: delayCalcIconSize() + + delegate: StandardItem { + height: trayView.itemHeight + width: trayView.itemWidth + + Image { + id: trayIcon + anchors.centerIn: parent +// source: iconName ? "image://icontheme/" + iconName +// : iconBytes ? "data:image/png;base64," + iconBytes +// : "image://icontheme/application-x-desktop" + + source: iconName ? "image://icontheme/" + iconName : "image://icontheme/application-x-desktop" + + width: root.trayItemSize + height: root.trayItemSize + sourceSize.width: root.trayItemSize + sourceSize.height: root.trayItemSize + asynchronous: true + } + + onClicked: trayModel.leftButtonClick(id) + onRightClicked: trayModel.rightButtonClick(id) + popupText: toolTip ? toolTip : title + } + } + + Item { + width: Meui.Units.smallSpacing + } + + ControlCenterItem { + onWidthChanged: delayCalcIconSize() + onHeightChanged: delayCalcIconSize() + } + } + + ControlCenter { + id: controlCenter + } + + Volume { + id: volume + } + + Battery { + id: battery + } + + NM.ConnectionIcon { + id: connectionIconProvider + } + + NM.Network { + id: network + } + + Connections { + target: Settings + + function onDirectionChanged() { + popupTips.hide() + } + } + + Connections { + target: mainWindow + + function onResizingFished() { + root.calcIconSize() + } + + function onIconSizeChanged() { + root.calcIconSize() + } + + function onPositionChanged() { + root.delayCalcIconSize() + } + } +} diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..3ec4aa3 --- /dev/null +++ b/resources.qrc @@ -0,0 +1,104 @@ + + + qml/main.qml + qml/DockItem.qml + qml/AppItem.qml + svg/launcher.svg + svg/light/audio-volume-high-symbolic.svg + svg/light/audio-volume-low-symbolic.svg + svg/light/audio-volume-medium-symbolic.svg + svg/light/audio-volume-muted-symbolic.svg + svg/light/battery-level-0-charging-symbolic.svg + svg/light/battery-level-0-symbolic.svg + svg/light/battery-level-10-charging-symbolic.svg + svg/light/battery-level-10-symbolic.svg + svg/light/battery-level-20-charging-symbolic.svg + svg/light/battery-level-20-symbolic.svg + svg/light/battery-level-30-charging-symbolic.svg + svg/light/battery-level-30-symbolic.svg + svg/light/battery-level-40-charging-symbolic.svg + svg/light/battery-level-40-symbolic.svg + svg/light/battery-level-50-charging-symbolic.svg + svg/light/battery-level-50-symbolic.svg + svg/light/battery-level-60-charging-symbolic.svg + svg/light/battery-level-60-symbolic.svg + svg/light/battery-level-70-charging-symbolic.svg + svg/light/battery-level-70-symbolic.svg + svg/light/battery-level-80-charging-symbolic.svg + svg/light/battery-level-80-symbolic.svg + svg/light/battery-level-90-charging-symbolic.svg + svg/light/battery-level-90-symbolic.svg + svg/light/battery-level-100-charging-symbolic.svg + svg/light/battery-level-100-symbolic.svg + svg/light/close_normal.svg + svg/light/control.svg + svg/light/minimize_normal.svg + svg/light/restore_normal.svg + svg/light/system-shutdown-symbolic.svg + svg/dark/audio-volume-high-symbolic.svg + svg/dark/audio-volume-low-symbolic.svg + svg/dark/audio-volume-medium-symbolic.svg + svg/dark/audio-volume-muted-symbolic.svg + svg/dark/battery-level-0-charging-symbolic.svg + svg/dark/battery-level-0-symbolic.svg + svg/dark/battery-level-10-charging-symbolic.svg + svg/dark/battery-level-10-symbolic.svg + svg/dark/battery-level-20-charging-symbolic.svg + svg/dark/battery-level-20-symbolic.svg + svg/dark/battery-level-30-charging-symbolic.svg + svg/dark/battery-level-30-symbolic.svg + svg/dark/battery-level-40-charging-symbolic.svg + svg/dark/battery-level-40-symbolic.svg + svg/dark/battery-level-50-charging-symbolic.svg + svg/dark/battery-level-50-symbolic.svg + svg/dark/battery-level-60-charging-symbolic.svg + svg/dark/battery-level-60-symbolic.svg + svg/dark/battery-level-70-charging-symbolic.svg + svg/dark/battery-level-70-symbolic.svg + svg/dark/battery-level-80-charging-symbolic.svg + svg/dark/battery-level-80-symbolic.svg + svg/dark/battery-level-90-charging-symbolic.svg + svg/dark/battery-level-90-symbolic.svg + svg/dark/battery-level-100-charging-symbolic.svg + svg/dark/battery-level-100-symbolic.svg + svg/dark/close_normal.svg + svg/dark/control.svg + svg/dark/minimize_normal.svg + svg/dark/restore_normal.svg + svg/dark/system-shutdown-symbolic.svg + qml/StandardItem.qml + qml/ControlCenter.qml + svg/dark/brightness.svg + svg/light/brightness.svg + qml/IconButton.qml + svg/light/settings.svg + svg/dark/settings.svg + svg/dark/network-wired.svg + svg/dark/network-wired-activated.svg + svg/dark/network-wireless-connected-00.svg + svg/dark/network-wireless-connected-25.svg + svg/dark/network-wireless-connected-50.svg + svg/dark/network-wireless-connected-75.svg + svg/dark/network-wireless-connected-100.svg + svg/light/network-wired.svg + svg/light/network-wired-activated.svg + svg/light/network-wireless-connected-00.svg + svg/light/network-wireless-connected-25.svg + svg/light/network-wireless-connected-50.svg + svg/light/network-wireless-connected-75.svg + svg/light/network-wireless-connected-100.svg + qml/CardItem.qml + svg/light/bluetooth-symbolic.svg + qml/MprisController.qml + svg/dark/media-playback-pause-symbolic.svg + svg/dark/media-playback-start-symbolic.svg + svg/dark/media-skip-backward-symbolic.svg + svg/dark/media-skip-forward-symbolic.svg + svg/light/media-playback-pause-symbolic.svg + svg/light/media-playback-start-symbolic.svg + svg/light/media-skip-backward-symbolic.svg + svg/light/media-skip-forward-symbolic.svg + qml/ControlCenterItem.qml + svg/light/dark-mode.svg + + diff --git a/src/appearance.cpp b/src/appearance.cpp new file mode 100644 index 0000000..8670f30 --- /dev/null +++ b/src/appearance.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2020 CyberOS Team. + * + * Author: revenmartin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "appearance.h" + +#include +#include +#include +#include +#include + +Appearance::Appearance(QObject *parent) + : QObject(parent) + , m_interface("org.cyber.Settings", + "/Theme", + "org.cyber.Theme", + QDBusConnection::sessionBus()) + , m_dockSettings(new QSettings(QSettings::UserScope, "cyberos", "dock")) + , m_dockConfigWacher(new QFileSystemWatcher(this)) + , m_dockIconSize(0) + , m_dockDirection(0) + , m_fontPointSize(11) +{ + m_dockIconSize = m_dockSettings->value("IconSize").toInt(); + m_dockDirection = m_dockSettings->value("Direction").toInt(); + + m_dockConfigWacher->addPath(m_dockSettings->fileName()); + connect(m_dockConfigWacher, &QFileSystemWatcher::fileChanged, this, [=] { + m_dockSettings->sync(); + m_dockIconSize = m_dockSettings->value("IconSize").toInt(); + m_dockDirection = m_dockSettings->value("Direction").toInt(); + m_dockConfigWacher->addPath(m_dockSettings->fileName()); + emit dockIconSizeChanged(); + emit dockDirectionChanged(); + }); + + // Init + if (m_interface.isValid()) { + m_fontPointSize = m_interface.property("systemFontPointSize").toInt(); + + connect(&m_interface, SIGNAL(darkModeDimsWallpaerChanged()), this, SIGNAL(dimsWallpaperChanged())); + } +} + +void Appearance::switchDarkMode(bool darkMode) +{ + if (m_interface.isValid()) { + m_interface.call("setDarkMode", darkMode); + } +} + +bool Appearance::dimsWallpaper() const +{ + return m_interface.property("darkModeDimsWallpaer").toBool(); +} + +void Appearance::setDimsWallpaper(bool value) +{ + m_interface.call("setDarkModeDimsWallpaer", value); +} + +int Appearance::dockIconSize() const +{ + return m_dockIconSize; +} + +void Appearance::setDockIconSize(int dockIconSize) +{ + if (m_dockIconSize == dockIconSize) + return; + + m_dockIconSize = dockIconSize; + m_dockSettings->setValue("IconSize", m_dockIconSize); +} + +int Appearance::dockDirection() const +{ + return m_dockDirection; +} + +void Appearance::setDockDirection(int dockDirection) +{ + if (m_dockDirection == dockDirection) + return; + + m_dockDirection = dockDirection; + m_dockSettings->setValue("Direction", m_dockDirection); +} + +void Appearance::setGenericFontFamily(const QString &name) +{ + if (name.isEmpty()) + return; + + QDBusInterface iface("org.cyber.Settings", + "/Theme", + "org.cyber.Theme", + QDBusConnection::sessionBus(), this); + if (iface.isValid()) { + iface.call("setSystemFont", name); + } +} + +void Appearance::setFixedFontFamily(const QString &name) +{ + if (name.isEmpty()) + return; + + QDBusInterface iface("org.cyber.Settings", + "/Theme", + "org.cyber.Theme", + QDBusConnection::sessionBus(), this); + if (iface.isValid()) { + iface.call("setSystemFixedFont", name); + } +} + +int Appearance::fontPointSize() const +{ + return m_fontPointSize; +} + +void Appearance::setFontPointSize(int fontPointSize) +{ + m_fontPointSize = fontPointSize; + + QDBusInterface iface("org.cyber.Settings", + "/Theme", + "org.cyber.Theme", + QDBusConnection::sessionBus(), this); + if (iface.isValid()) { + iface.call("setSystemFontPointSize", m_fontPointSize * 1.0); + } +} + +void Appearance::setAccentColor(int accentColor) +{ + QDBusInterface iface("org.cyber.Settings", + "/Theme", + "org.cyber.Theme", + QDBusConnection::sessionBus(), this); + if (iface.isValid()) { + iface.call("setAccentColor", accentColor); + } +} + +double Appearance::devicePixelRatio() const +{ + return m_interface.property("devicePixelRatio").toDouble(); +} + +void Appearance::setDevicePixelRatio(double value) +{ + QDBusInterface iface("org.cyber.Settings", + "/Theme", + "org.cyber.Theme", + QDBusConnection::sessionBus(), this); + if (iface.isValid()) { + iface.call("setDevicePixelRatio", value); + } +} diff --git a/src/appearance.h b/src/appearance.h new file mode 100644 index 0000000..fa96055 --- /dev/null +++ b/src/appearance.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2020 CyberOS Team. + * + * Author: revenmartin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef APPEARANCE_H +#define APPEARANCE_H + +#include +#include +#include +#include + +class Appearance : public QObject +{ + Q_OBJECT + Q_PROPERTY(int dockIconSize READ dockIconSize WRITE setDockIconSize NOTIFY dockIconSizeChanged) + Q_PROPERTY(int dockDirection READ dockDirection WRITE setDockDirection NOTIFY dockDirectionChanged) + Q_PROPERTY(int fontPointSize READ fontPointSize WRITE setFontPointSize NOTIFY fontPointSizeChanged) + Q_PROPERTY(bool dimsWallpaper READ dimsWallpaper WRITE setDimsWallpaper NOTIFY dimsWallpaperChanged) + Q_PROPERTY(double devicePixelRatio READ devicePixelRatio WRITE setDevicePixelRatio NOTIFY devicePixelRatioChanged) + +public: + explicit Appearance(QObject *parent = nullptr); + + Q_INVOKABLE void switchDarkMode(bool darkMode); + + bool dimsWallpaper() const; + Q_INVOKABLE void setDimsWallpaper(bool value); + + int dockIconSize() const; + Q_INVOKABLE void setDockIconSize(int dockIconSize); + + int dockDirection() const; + Q_INVOKABLE void setDockDirection(int dockDirection); + + Q_INVOKABLE void setGenericFontFamily(const QString &name); + Q_INVOKABLE void setFixedFontFamily(const QString &name); + + int fontPointSize() const; + Q_INVOKABLE void setFontPointSize(int fontPointSize); + + Q_INVOKABLE void setAccentColor(int accentColor); + + double devicePixelRatio() const; + Q_INVOKABLE void setDevicePixelRatio(double value); + +signals: + void dockIconSizeChanged(); + void dockDirectionChanged(); + void fontPointSizeChanged(); + void dimsWallpaperChanged(); + void devicePixelRatioChanged(); + +private: + QDBusInterface m_interface; + QSettings *m_dockSettings; + QFileSystemWatcher *m_dockConfigWacher; + + int m_dockIconSize; + int m_dockDirection; + int m_fontPointSize; +}; + +#endif // APPEARANCE_H diff --git a/src/applicationitem.h b/src/applicationitem.h new file mode 100644 index 0000000..c5d3c3d --- /dev/null +++ b/src/applicationitem.h @@ -0,0 +1,29 @@ +#ifndef APPLICATIONITEM_H +#define APPLICATIONITEM_H + +#include + +class ApplicationItem +{ +public: + // window class + QString id; + // icon name + QString iconName; + // visible name + QString visibleName; + QString desktopPath; + QString exec; + + QList wids; + + int currentActive = 0; + bool isActive = false; + bool isPinned = false; + + bool operator==(ApplicationItem item) { + return item.id == this->id; + } +}; + +#endif // APPLICATIONITEM_H diff --git a/src/applicationmodel.cpp b/src/applicationmodel.cpp new file mode 100644 index 0000000..c627e29 --- /dev/null +++ b/src/applicationmodel.cpp @@ -0,0 +1,456 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "applicationmodel.h" +#include "utils.h" + +#include + +ApplicationModel::ApplicationModel(QObject *parent) + : QAbstractListModel(parent) + , m_iface(XWindowInterface::instance()) + , m_sysAppMonitor(SystemAppMonitor::self()) +{ + m_sysAppMonitor->moveToThread(qApp->thread()); + this->moveToThread(qApp->thread()); + + connect(m_iface, &XWindowInterface::windowAdded, this, &ApplicationModel::onWindowAdded); + connect(m_iface, &XWindowInterface::windowRemoved, this, &ApplicationModel::onWindowRemoved); + connect(m_iface, &XWindowInterface::activeChanged, this, &ApplicationModel::onActiveChanged); + + initPinnedApplications(); + + QTimer::singleShot(100, m_iface, &XWindowInterface::startInitWindows); +} + +int ApplicationModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return m_appItems.size(); +} + +QHash ApplicationModel::roleNames() const +{ + QHash roles; + roles[AppIdRole] = "appId"; + roles[IconNameRole] = "iconName"; + roles[VisibleNameRole] = "visibleName"; + roles[ActiveRole] = "isActive"; + roles[WindowCountRole] = "windowCount"; + roles[IsPinnedRole] = "isPinned"; + return roles; +} + +QVariant ApplicationModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + ApplicationItem *item = m_appItems.at(index.row()); + + switch (role) { + case AppIdRole: + return item->id; + case IconNameRole: + return item->iconName; + case VisibleNameRole: + return item->visibleName; + case ActiveRole: + return item->isActive; + case WindowCountRole: + return item->wids.count(); + case IsPinnedRole: + return item->isPinned; + default: + return QVariant(); + } + + return QVariant(); +} + +void ApplicationModel::clicked(const QString &id) +{ + ApplicationItem *item = findItemById(id); + + if (!item) + return; + + // Application Item that has been pinned, + // We need to open it. + if (item->wids.isEmpty()) { + // open application + openNewInstance(item->id); + } + // Multiple windows have been opened and need to switch between them, + // The logic here needs to be improved. + else if (item->wids.count() > 1) { + item->currentActive++; + + if (item->currentActive == item->wids.count()) + item->currentActive = 0; + + m_iface->forceActiveWindow(item->wids.at(item->currentActive)); + } else if (m_iface->activeWindow() == item->wids.first()) { + m_iface->minimizeWindow(item->wids.first()); + } else { + m_iface->forceActiveWindow(item->wids.first()); + } +} + +void ApplicationModel::raiseWindow(const QString &id) +{ + ApplicationItem *item = findItemById(id); + + if (!item) + return; + + m_iface->forceActiveWindow(item->wids.at(item->currentActive)); +} + +bool ApplicationModel::openNewInstance(const QString &appId) +{ + ApplicationItem *item = findItemById(appId); + + if (!item) + return false; + + QProcess process; + if (!item->exec.isEmpty()) { + QStringList args = item->exec.split(" "); + process.setProgram(args.first()); + args.removeFirst(); + + if (!args.isEmpty()) { + process.setArguments(args); + } + + } else { + process.setProgram(appId); + } + + return process.startDetached(); +} + +void ApplicationModel::closeAllByAppId(const QString &appId) +{ + ApplicationItem *item = findItemById(appId); + + if (!item) + return; + + for (quint64 wid : item->wids) { + m_iface->closeWindow(wid); + } +} + +void ApplicationModel::pin(const QString &appId) +{ + ApplicationItem *item = findItemById(appId); + + if (!item) + return; + + item->isPinned = true; + + handleDataChangedFromItem(item); + savePinAndUnPinList(); +} + +void ApplicationModel::unPin(const QString &appId) +{ + ApplicationItem *item = findItemById(appId); + + if (!item) + return; + + item->isPinned = false; + handleDataChangedFromItem(item); + + // Need to be removed after unpin + if (item->wids.isEmpty()) { + int index = indexOf(item->id); + if (index != -1) { + beginRemoveRows(QModelIndex(), index, index); + m_appItems.removeAll(item); + endRemoveRows(); + + emit itemRemoved(); + emit countChanged(); + } + } + + savePinAndUnPinList(); +} + +void ApplicationModel::updateGeometries(const QString &id, QRect rect) +{ + ApplicationItem *item = findItemById(id); + + // If not found + if (!item) + return; + + for (quint64 id : item->wids) { + m_iface->setIconGeometry(id, rect); + } +} + +void ApplicationModel::move(int from, int to) +{ + if (from == to) + return; + + m_appItems.move(from, to); + + if (from < to) + beginMoveRows(QModelIndex(), from, from, QModelIndex(), to + 1); + else + beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); + + endMoveRows(); +} + +ApplicationItem *ApplicationModel::findItemByWId(quint64 wid) +{ + for (ApplicationItem *item : m_appItems) { + for (quint64 winId : item->wids) { + if (winId == wid) + return item; + } + } + + return nullptr; +} + +ApplicationItem *ApplicationModel::findItemById(const QString &id) +{ + for (ApplicationItem *item : m_appItems) { + if (item->id == id) + return item; + } + + return nullptr; +} + +bool ApplicationModel::contains(const QString &id) +{ + for (ApplicationItem *item : qAsConst(m_appItems)) { + if (item->id == id) + return true; + } + + return false; +} + +int ApplicationModel::indexOf(const QString &id) +{ + for (ApplicationItem *item : m_appItems) { + if (item->id == id) + return m_appItems.indexOf(item); + } + + return -1; +} + +void ApplicationModel::initPinnedApplications() +{ + QSettings settings(QSettings::UserScope, "cyberos", "dock_pinned"); + QStringList groups = settings.childGroups(); + + // Launcher + ApplicationItem *item = new ApplicationItem; + item->id = "cyber-launcher"; + item->exec = "cyber-launcher"; + item->iconName = "qrc:/svg/launcher.svg"; + item->visibleName = tr("Launcher"); + m_appItems.append(item); + + // Pinned Apps + for (int i = 0; i < groups.size(); ++i) { + for (const QString &id : groups) { + settings.beginGroup(id); + int index = settings.value("Index").toInt(); + + if (index == i) { + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + ApplicationItem *item = new ApplicationItem; + + item->desktopPath = settings.value("DesktopPath").toString(); + item->id = id; + item->isPinned = true; + + // Read from desktop file. + if (!item->desktopPath.isEmpty()) { + QMap desktopInfo = Utils::instance()->readInfoFromDesktop(item->desktopPath); + item->iconName = desktopInfo.value("Icon"); + item->visibleName = desktopInfo.value("Name"); + item->exec = desktopInfo.value("Exec"); + } + + // Read from config file. + if (item->iconName.isEmpty()) + item->iconName = settings.value("Icon").toString(); + + if (item->visibleName.isEmpty()) + item->visibleName = settings.value("VisibleName").toString(); + + if (item->exec.isEmpty()) + item->exec = settings.value("Exec").toString(); + + m_appItems.append(item); + endInsertRows(); + + emit itemAdded(); + emit countChanged(); + + settings.endGroup(); + break; + } else { + settings.endGroup(); + } + } + } +} + +void ApplicationModel::savePinAndUnPinList() +{ + QSettings settings(QSettings::UserScope, "cyberos", "dock_pinned"); + settings.clear(); + + int index = 0; + + for (ApplicationItem *item : m_appItems) { + if (item->isPinned) { + settings.beginGroup(item->id); + settings.setValue("Index", index); + settings.setValue("Icon", item->iconName); + settings.setValue("VisibleName", item->visibleName); + settings.setValue("Exec", item->exec); + settings.setValue("DesktopPath", item->desktopPath); + settings.endGroup(); + ++index; + } + } + + settings.sync(); +} + +void ApplicationModel::handleDataChangedFromItem(ApplicationItem *item) +{ + if (!item) + return; + + QModelIndex idx = index(indexOf(item->id), 0, QModelIndex()); + + if (idx.isValid()) { + emit dataChanged(idx, idx); + } +} + +void ApplicationModel::onWindowAdded(quint64 wid) +{ + QMap info = m_iface->requestInfo(wid); + const QString id = info.value("id").toString(); + + if (contains(id)) { + for (ApplicationItem *item : m_appItems) { + if (item->id == id) { + item->wids.append(wid); + // Need to update application active status. + item->isActive = info.value("active").toBool(); + handleDataChangedFromItem(item); + } + } + } else { + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + ApplicationItem *item = new ApplicationItem; + item->id = id; + item->iconName = info.value("iconName").toString(); + item->visibleName = info.value("visibleName").toString(); + item->isActive = info.value("active").toBool(); + item->wids.append(wid); + + QString desktopPath = m_iface->desktopFilePath(wid); + + if (!desktopPath.isEmpty()) { + QMap desktopInfo = Utils::instance()->readInfoFromDesktop(desktopPath); + item->iconName = desktopInfo.value("Icon"); + item->visibleName = desktopInfo.value("Name"); + item->exec = desktopInfo.value("Exec"); + item->desktopPath = desktopPath; + } + + m_appItems << item; + endInsertRows(); + + emit itemAdded(); + emit countChanged(); + } +} + +void ApplicationModel::onWindowRemoved(quint64 wid) +{ + ApplicationItem *item = findItemByWId(wid); + + if (!item) + return; + + // Remove from wid list. + item->wids.removeOne(wid); + + if (item->currentActive >= item->wids.size()) + item->currentActive = 0; + + handleDataChangedFromItem(item); + + if (item->wids.isEmpty()) { + // If it is not fixed to the dock, need to remove it. + if (!item->isPinned) { + int index = indexOf(item->id); + + if (index == -1) + return; + + beginRemoveRows(QModelIndex(), index, index); + m_appItems.removeAll(item); + endRemoveRows(); + + emit itemRemoved(); + emit countChanged(); + } + } +} + +void ApplicationModel::onActiveChanged(quint64 wid) +{ + // Using this method will cause the listview scrollbar to reset. + // beginResetModel(); + + for (ApplicationItem *item : m_appItems) { + if (item->isActive != item->wids.contains(wid)) { + item->isActive = item->wids.contains(wid); + + QModelIndex idx = index(indexOf(item->id), 0, QModelIndex()); + if (idx.isValid()) { + emit dataChanged(idx, idx); + } + } + } +} diff --git a/src/applicationmodel.h b/src/applicationmodel.h new file mode 100644 index 0000000..81f018f --- /dev/null +++ b/src/applicationmodel.h @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef APPLICATIONMODEL_H +#define APPLICATIONMODEL_H + +#include +#include "applicationitem.h" +#include "systemappmonitor.h" +#include "xwindowinterface.h" + +class ApplicationModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + AppIdRole = Qt::UserRole + 1, + IconNameRole, + VisibleNameRole, + ActiveRole, + WindowCountRole, + IsPinnedRole + }; + + explicit ApplicationModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_INVOKABLE void save() { savePinAndUnPinList(); } + + Q_INVOKABLE void clicked(const QString &id); + Q_INVOKABLE void raiseWindow(const QString &id); + + Q_INVOKABLE bool openNewInstance(const QString &appId); + Q_INVOKABLE void closeAllByAppId(const QString &appId); + Q_INVOKABLE void pin(const QString &appId); + Q_INVOKABLE void unPin(const QString &appId); + + Q_INVOKABLE void updateGeometries(const QString &id, QRect rect); + + Q_INVOKABLE void move(int from, int to); + +signals: + void countChanged(); + + void itemAdded(); + void itemRemoved(); + +private: + ApplicationItem *findItemByWId(quint64 wid); + ApplicationItem *findItemById(const QString &id); + bool contains(const QString &id); + int indexOf(const QString &id); + void initPinnedApplications(); + void savePinAndUnPinList(); + + void handleDataChangedFromItem(ApplicationItem *item); + + void onWindowAdded(quint64 wid); + void onWindowRemoved(quint64 wid); + void onActiveChanged(quint64 wid); + +private: + XWindowInterface *m_iface; + SystemAppMonitor *m_sysAppMonitor; + QList m_appItems; +}; + +#endif // APPLICATIONMODEL_H diff --git a/src/battery.cpp b/src/battery.cpp new file mode 100644 index 0000000..5c8e549 --- /dev/null +++ b/src/battery.cpp @@ -0,0 +1,130 @@ +#include "battery.h" + +static const QString s_sServer = "org.cyber.Settings"; +static const QString s_sPath = "/PrimaryBattery"; +static const QString s_sInterface = "org.cyber.PrimaryBattery"; + +Battery::Battery(QObject *parent) + : QObject(parent) + , m_upowerInterface("org.freedesktop.UPower", + "/org/freedesktop/UPower", + "org.freedesktop.UPower", + QDBusConnection::systemBus()) + , m_interface("org.cyber.Settings", + "/PrimaryBattery", + "org.cyber.PrimaryBattery", + QDBusConnection::sessionBus()) + , m_available(false) + , m_onBattery(false) +{ + m_available = m_interface.isValid() && !m_interface.lastError().isValid(); + + if (m_available) { + QDBusConnection::sessionBus().connect(s_sServer, s_sPath, s_sInterface, "chargeStateChanged", this, SLOT(chargeStateChanged(int))); + QDBusConnection::sessionBus().connect(s_sServer, s_sPath, s_sInterface, "chargePercentChanged", this, SLOT(chargePercentChanged(int))); + QDBusConnection::sessionBus().connect(s_sServer, s_sPath, s_sInterface, "lastChargedPercentChanged", this, SLOT(lastChargedPercentChanged())); + QDBusConnection::sessionBus().connect(s_sServer, s_sPath, s_sInterface, "capacityChanged", this, SLOT(capacityChanged(int))); + QDBusConnection::sessionBus().connect(s_sServer, s_sPath, s_sInterface, "remainingTimeChanged", this, SLOT(remainingTimeChanged(qlonglong))); + + // Update Icon + QDBusConnection::sessionBus().connect(s_sServer, s_sPath, s_sInterface, "chargePercentChanged", this, SLOT(iconSourceChanged())); + + QDBusInterface interface("org.freedesktop.UPower", "/org/freedesktop/UPower", + "org.freedesktop.UPower", QDBusConnection::systemBus()); + + QDBusConnection::systemBus().connect("org.freedesktop.UPower", "/org/freedesktop/UPower", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", this, + SLOT(onPropertiesChanged(QString, QVariantMap, QStringList))); + + if (interface.isValid()) { + m_onBattery = interface.property("OnBattery").toBool(); + } + + emit validChanged(); + } +} + +bool Battery::available() const +{ + return m_available; +} + +bool Battery::onBattery() const +{ + return m_onBattery; +} + +int Battery::chargeState() const +{ + return m_interface.property("chargeState").toInt(); +} + +int Battery::chargePercent() const +{ + return m_interface.property("chargePercent").toInt(); +} + +int Battery::lastChargedPercent() const +{ + return m_interface.property("lastChargedPercent").toInt(); +} + +int Battery::capacity() const +{ + return m_interface.property("capacity").toInt(); +} + +QString Battery::statusString() const +{ + return m_interface.property("statusString").toString(); +} + +QString Battery::iconSource() const +{ + int percent = this->chargePercent(); + int range = 0; + + if (percent >= 95) + range = 100; + else if (percent >= 85) + range = 90; + else if (percent>= 75) + range = 80; + else if (percent >= 65) + range = 70; + else if (percent >= 55) + range = 60; + else if (percent >= 45) + range = 50; + else if (percent >= 35) + range = 40; + else if (percent >= 25) + range = 30; + else if (percent >= 15) + range = 20; + else if (percent >= 5) + range = 10; + else + range = 0; + + if (m_onBattery) + return QString("battery-level-%1-symbolic.svg").arg(range); + + return QString("battery-level-%1-charging-symbolic.svg").arg(range); +} + +void Battery::onPropertiesChanged(const QString &ifaceName, const QVariantMap &changedProps, const QStringList &invalidatedProps) +{ + Q_UNUSED(ifaceName); + Q_UNUSED(changedProps); + Q_UNUSED(invalidatedProps); + + bool onBattery = m_upowerInterface.property("OnBattery").toBool(); + if (onBattery != m_onBattery) { + m_onBattery = onBattery; + m_interface.call("refresh"); + emit onBatteryChanged(); + emit iconSourceChanged(); + } +} diff --git a/src/battery.h b/src/battery.h new file mode 100644 index 0000000..349e2b6 --- /dev/null +++ b/src/battery.h @@ -0,0 +1,53 @@ +#ifndef BATTERY_H +#define BATTERY_H + +#include +#include + +class Battery : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool available READ available NOTIFY validChanged) + Q_PROPERTY(int chargeState READ chargeState NOTIFY chargeStateChanged) + Q_PROPERTY(int chargePercent READ chargePercent NOTIFY chargePercentChanged) + Q_PROPERTY(int lastChargedPercent READ lastChargedPercent NOTIFY lastChargedPercentChanged) + Q_PROPERTY(int capacity READ capacity NOTIFY capacityChanged) + Q_PROPERTY(QString statusString READ statusString NOTIFY remainingTimeChanged) + Q_PROPERTY(bool onBattery READ onBattery NOTIFY onBatteryChanged) + Q_PROPERTY(QString iconSource READ iconSource NOTIFY iconSourceChanged) + +public: + explicit Battery(QObject *parent = nullptr); + + bool available() const; + bool onBattery() const; + + int chargeState() const; + int chargePercent() const; + int lastChargedPercent() const; + int capacity() const; + QString statusString() const; + + QString iconSource() const; + +signals: + void validChanged(); + void chargeStateChanged(int); + void chargePercentChanged(int); + void capacityChanged(int); + void remainingTimeChanged(qlonglong time); + void onBatteryChanged(); + void lastChargedPercentChanged(); + void iconSourceChanged(); + +private slots: + void onPropertiesChanged(const QString &ifaceName, const QVariantMap &changedProps, const QStringList &invalidatedProps); + +private: + QDBusInterface m_upowerInterface; + QDBusInterface m_interface; + bool m_available; + bool m_onBattery; +}; + +#endif // BATTERY_H diff --git a/src/brightness.cpp b/src/brightness.cpp new file mode 100644 index 0000000..fa743ce --- /dev/null +++ b/src/brightness.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2020 CyberOS Team. + * + * Author: revenmartin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "brightness.h" + +Brightness::Brightness(QObject *parent) + : QObject(parent) + , m_dbusConnection(QDBusConnection::sessionBus()) + , m_iface("org.cyber.Settings", + "/Brightness", + "org.cyber.Brightness", m_dbusConnection) + , m_value(0) + , m_enabled(false) +{ + if (!m_iface.isValid()) + return; + + m_value = m_iface.property("brightness").toInt(); + m_enabled = m_iface.property("brightnessEnabled").toBool(); +} + +void Brightness::setValue(int value) +{ + m_iface.call("setValue", value); +} + +int Brightness::value() const +{ + return m_value; +} + +bool Brightness::enabled() const +{ + return m_enabled; +} diff --git a/src/brightness.h b/src/brightness.h new file mode 100644 index 0000000..c674ab5 --- /dev/null +++ b/src/brightness.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2020 CyberOS Team. + * + * Author: revenmartin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef BRIGHTNESS_H +#define BRIGHTNESS_H + +#include +#include + +class Brightness : public QObject +{ + Q_OBJECT + Q_PROPERTY(int value READ value NOTIFY valueChanged) + Q_PROPERTY(bool enabled READ enabled CONSTANT) + +public: + explicit Brightness(QObject *parent = nullptr); + + Q_INVOKABLE void setValue(int value); + + int value() const; + bool enabled() const; + +signals: + void valueChanged(); + +private: + QDBusConnection m_dbusConnection; + QDBusInterface m_iface; + int m_value; + bool m_enabled; +}; + +#endif // BRIGHTNESS_H diff --git a/src/controlcenterdialog.cpp b/src/controlcenterdialog.cpp new file mode 100644 index 0000000..d3eb9fe --- /dev/null +++ b/src/controlcenterdialog.cpp @@ -0,0 +1,25 @@ +#include "controlcenterdialog.h" +#include "xwindowinterface.h" +#include + +ControlCenterDialog::ControlCenterDialog(QQuickView *parent) + : QQuickView(parent) +{ + setFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); + + connect(this, &QQuickView::activeChanged, this, [=] { + if (!isActive()) + hide(); + }); +} + +void ControlCenterDialog::showEvent(QShowEvent *event) +{ + KWindowSystem::setState(winId(), NET::SkipTaskbar | NET::SkipPager | NET::SkipSwitcher); + QQuickView::showEvent(event); +} + +void ControlCenterDialog::hideEvent(QHideEvent *event) +{ + QQuickView::hideEvent(event); +} diff --git a/src/controlcenterdialog.h b/src/controlcenterdialog.h new file mode 100644 index 0000000..adeb18e --- /dev/null +++ b/src/controlcenterdialog.h @@ -0,0 +1,19 @@ +#ifndef CONTROLCENTERDIALOG_H +#define CONTROLCENTERDIALOG_H + +#include +#include + +class ControlCenterDialog : public QQuickView +{ + Q_OBJECT + +public: + ControlCenterDialog(QQuickView *view = nullptr); + +protected: + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; +}; + +#endif // CONTROLCENTERDIALOG_H diff --git a/src/docksettings.cpp b/src/docksettings.cpp new file mode 100644 index 0000000..e332d4b --- /dev/null +++ b/src/docksettings.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "docksettings.h" + +#include +#include +#include + +#include +#include + +static DockSettings *SELF = nullptr; + +DockSettings *DockSettings::self() +{ + if (SELF == nullptr) + SELF = new DockSettings; + + return SELF; +} + +DockSettings::DockSettings(QObject *parent) + : QObject(parent) + , m_iconSize(0) + , m_edgeMargins(10) + , m_statusBarHeight(30) + , m_direction(Left) + , m_visibility(AlwaysVisible) + , m_settings(new QSettings(QSettings::UserScope, "cyberos", "dock")) + , m_fileWatcher(new QFileSystemWatcher(this)) +{ + if (!m_settings->contains("IconSize")) + m_settings->setValue("IconSize", 64); + if (!m_settings->contains("Direction")) + m_settings->setValue("Direction", Bottom); + if (!m_settings->contains("Visibility")) + m_settings->setValue("Visibility", AlwaysVisible); + + m_settings->sync(); + + m_iconSize = m_settings->value("IconSize").toInt(); + m_direction = static_cast(m_settings->value("Direction").toInt()); + + m_fileWatcher->addPath(m_settings->fileName()); + connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &DockSettings::onConfigFileChanged); +} + +int DockSettings::iconSize() const +{ + return m_iconSize; +} + +void DockSettings::setIconSize(int iconSize) +{ + m_iconSize = iconSize; + emit iconSizeChanged(); +} + +DockSettings::Direction DockSettings::direction() const +{ + return m_direction; +} + +void DockSettings::setDirection(const Direction &direction) +{ + m_direction = direction; + emit directionChanged(); +} + +DockSettings::Visibility DockSettings::visibility() const +{ + return m_visibility; +} + +void DockSettings::setVisibility(const DockSettings::Visibility &visibility) +{ + if (m_visibility != visibility) { + m_visibility = visibility; + emit visibilityChanged(); + } +} + +int DockSettings::edgeMargins() const +{ + return m_edgeMargins; +} + +void DockSettings::setEdgeMargins(int edgeMargins) +{ + m_edgeMargins = edgeMargins; +} + +int DockSettings::statusBarHeight() const +{ + return m_statusBarHeight; +} + +void DockSettings::setStatusBarHeight(int statusBarHeight) +{ + m_statusBarHeight = statusBarHeight; +} + +void DockSettings::onConfigFileChanged() +{ + if (!QFile(m_settings->fileName()).exists()) + return; + + m_settings->sync(); + + int iconSize = m_settings->value("IconSize").toInt(); + Direction direction = static_cast(m_settings->value("Direction").toInt()); + Visibility visibility = static_cast(m_settings->value("Visibility").toInt()); + + if (m_iconSize != iconSize) + setIconSize(iconSize); + + if (m_direction != direction) + setDirection(direction); + + if (m_visibility != visibility) + setVisibility(visibility); + + m_fileWatcher->addPath(m_settings->fileName()); +} diff --git a/src/docksettings.h b/src/docksettings.h new file mode 100644 index 0000000..93343eb --- /dev/null +++ b/src/docksettings.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef DOCKSETTINGS_H +#define DOCKSETTINGS_H + +#include +#include +#include + +class DockSettings : public QObject +{ + Q_OBJECT + Q_PROPERTY(Direction direction READ direction WRITE setDirection NOTIFY directionChanged) + Q_PROPERTY(int iconSize READ iconSize WRITE setIconSize NOTIFY iconSizeChanged) + Q_PROPERTY(int edgeMargins READ edgeMargins WRITE setEdgeMargins) + +public: + enum Direction { + Left = 0, + Bottom + }; + Q_ENUMS(Direction) + + enum Visibility { + AlwaysVisible = 0, + AutoHide, + AlwaysHide + }; + Q_ENUMS(Visibility) + + static DockSettings *self(); + explicit DockSettings(QObject *parent = nullptr); + + int iconSize() const; + void setIconSize(int iconSize); + + Direction direction() const; + void setDirection(const Direction &direction); + + Visibility visibility() const; + void setVisibility(const Visibility &visibility); + + int edgeMargins() const; + void setEdgeMargins(int edgeMargins); + + int statusBarHeight() const; + void setStatusBarHeight(int statusBarHeight); + +private slots: + void onConfigFileChanged(); + +signals: + void iconSizeChanged(); + void directionChanged(); + void visibilityChanged(); + +private: + int m_iconSize; + int m_edgeMargins; + int m_statusBarHeight; + Direction m_direction; + Visibility m_visibility; + QSettings *m_settings; + QFileSystemWatcher *m_fileWatcher; +}; + +#endif // DOCKSETTINGS_H diff --git a/src/fakewindow.cpp b/src/fakewindow.cpp new file mode 100644 index 0000000..14c55b6 --- /dev/null +++ b/src/fakewindow.cpp @@ -0,0 +1,98 @@ +#include "fakewindow.h" +#include "docksettings.h" + +// Qt +#include +#include +#include +#include +#include +#include + +#include + +// X11 +#include + +FakeWindow::FakeWindow(QQuickView *parent) + : QQuickView(parent) + , m_delayedContainsMouse(false) + , m_containsMouse(false) +{ + setColor(Qt::red); + setDefaultAlphaBuffer(true); + setFlags(Qt::FramelessWindowHint | + Qt::WindowStaysOnTopHint | + Qt::NoDropShadowWindowHint | + Qt::WindowDoesNotAcceptFocus); + setScreen(qApp->primaryScreen()); + updateGeometry(); + show(); + + m_delayedMouseTimer.setSingleShot(true); + m_delayedMouseTimer.setInterval(50); + connect(&m_delayedMouseTimer, &QTimer::timeout, this, [this]() { + if (m_delayedContainsMouse) { + setContainsMouse(true); + } else { + setContainsMouse(false); + } + }); + + connect(DockSettings::self(), &DockSettings::directionChanged, this, &FakeWindow::updateGeometry); +} + +bool FakeWindow::containsMouse() const +{ + return m_containsMouse; +} + +bool FakeWindow::event(QEvent *e) +{ + if (e->type() == QEvent::DragEnter || e->type() == QEvent::DragMove) { + if (!m_containsMouse) { + m_delayedContainsMouse = false; + m_delayedMouseTimer.stop(); + setContainsMouse(true); + emit dragEntered(); + } + } else if (e->type() == QEvent::Enter) { + m_delayedContainsMouse = true; + if (!m_delayedMouseTimer.isActive()) { + m_delayedMouseTimer.start(); + } + } else if (e->type() == QEvent::Leave || e->type() == QEvent::DragLeave) { + m_delayedContainsMouse = false; + if (!m_delayedMouseTimer.isActive()) { + m_delayedMouseTimer.start(); + } + } + + return QQuickView::event(e); +} + +void FakeWindow::setContainsMouse(bool contains) +{ + if (m_containsMouse != contains) { + m_containsMouse = contains; + emit containsMouseChanged(contains); + } +} + +void FakeWindow::updateGeometry() +{ + int length = 10; + const QRect screenRect = qApp->primaryScreen()->geometry(); + QRect newRect; + + if (DockSettings::self()->direction() == DockSettings::Left) { + newRect = QRect(screenRect.x() - (length * 2), (screenRect.height() + length) / 2, + length, screenRect.height()); + } else if (DockSettings::self()->direction() == DockSettings::Bottom) { + newRect = QRect(screenRect.x(), + screenRect.y() + screenRect.height() - length, + screenRect.width(), length); + } + + setGeometry(newRect); +} diff --git a/src/fakewindow.h b/src/fakewindow.h new file mode 100644 index 0000000..5fea725 --- /dev/null +++ b/src/fakewindow.h @@ -0,0 +1,34 @@ +#ifndef FAKEWINDOW_H +#define FAKEWINDOW_H + +#include +#include + +class FakeWindow : public QQuickView +{ + Q_OBJECT + +public: + explicit FakeWindow(QQuickView *parent = nullptr); + + bool containsMouse() const; + +signals: + void containsMouseChanged(bool contains); + void dragEntered(); + +protected: + bool event(QEvent *e) override; + +private: + void setContainsMouse(bool contains); + void updateGeometry(); + +private: + QTimer m_delayedMouseTimer; + + bool m_delayedContainsMouse; + bool m_containsMouse; +}; + +#endif // FAKEWINDOW_H diff --git a/src/iconthemeimageprovider.cpp b/src/iconthemeimageprovider.cpp new file mode 100644 index 0000000..b649c51 --- /dev/null +++ b/src/iconthemeimageprovider.cpp @@ -0,0 +1,33 @@ +#include "iconthemeimageprovider.h" +#include + +IconThemeImageProvider::IconThemeImageProvider() + : QQuickImageProvider(QQuickImageProvider::Pixmap) +{ +} + +QPixmap IconThemeImageProvider::requestPixmap(const QString &id, QSize *realSize, + const QSize &requestedSize) +{ + // Sanitize requested size + QSize size(requestedSize); + if (size.width() < 1) + size.setWidth(1); + if (size.height() < 1) + size.setHeight(1); + + // Return real size + if (realSize) + *realSize = size; + + // Is it a path? + if (id.startsWith(QLatin1Char('/'))) + return QPixmap(id).scaled(size); + + // Return icon from theme or fallback to a generic icon + QIcon icon = QIcon::fromTheme(id); + if (icon.isNull()) + icon = QIcon::fromTheme(QLatin1String("application-x-desktop")); + + return icon.pixmap(size); +} diff --git a/src/iconthemeimageprovider.h b/src/iconthemeimageprovider.h new file mode 100644 index 0000000..e8cfe14 --- /dev/null +++ b/src/iconthemeimageprovider.h @@ -0,0 +1,14 @@ +#ifndef ICONTHEMEIMAGEPROVIDER_H +#define ICONTHEMEIMAGEPROVIDER_H + +#include + +class IconThemeImageProvider : public QQuickImageProvider +{ +public: + IconThemeImageProvider(); + + QPixmap requestPixmap(const QString &id, QSize *realSize, const QSize &requestedSize); +}; + +#endif // ICONTHEMEIMAGEPROVIDER_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..5176704 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#include "applicationmodel.h" +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication app(argc, argv); + + QString qmFilePath = QString("%1/%2.qm").arg("/usr/share/cyber-dock/translations/").arg(QLocale::system().name()); + if (QFile::exists(qmFilePath)) { + QTranslator *translator = new QTranslator(QApplication::instance()); + if (translator->load(qmFilePath)) { + QGuiApplication::installTranslator(translator); + } else { + translator->deleteLater(); + } + } + + MainWindow w; + + return app.exec(); +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp new file mode 100644 index 0000000..a5fbd0b --- /dev/null +++ b/src/mainwindow.cpp @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "mainwindow.h" +#include "iconthemeimageprovider.h" +#include "processprovider.h" +#include "volumemanager.h" +#include "battery.h" +#include "brightness.h" +#include "controlcenterdialog.h" +#include "statusnotifier/statusnotifiermodel.h" +#include "appearance.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +MainWindow::MainWindow(QQuickView *parent) + : QQuickView(parent) + , m_settings(DockSettings::self()) + , m_appModel(new ApplicationModel) + , m_fakeWindow(nullptr) +{ + qmlRegisterType("Cyber.Dock", 1, 0, "DockSettings"); + qmlRegisterType("Cyber.Dock", 1, 0, "Volume"); + qmlRegisterType("Cyber.Dock", 1, 0, "Battery"); + qmlRegisterType("Cyber.Dock", 1, 0, "Brightness"); + qmlRegisterType("Cyber.Dock", 1, 0, "ControlCenterDialog"); + qmlRegisterType("Cyber.Dock", 1, 0, "StatusNotifierModel"); + qmlRegisterType("Cyber.Dock", 1, 0, "Appearance"); + + setDefaultAlphaBuffer(false); + setColor(Qt::transparent); + + setFlags(Qt::FramelessWindowHint | Qt::WindowDoesNotAcceptFocus); + KWindowSystem::setOnDesktop(winId(), NET::OnAllDesktops); + KWindowSystem::setType(winId(), NET::Dock); + + engine()->rootContext()->setContextProperty("appModel", m_appModel); + engine()->rootContext()->setContextProperty("process", new ProcessProvider); + engine()->rootContext()->setContextProperty("Settings", m_settings); + engine()->rootContext()->setContextProperty("mainWindow", this); + + setResizeMode(QQuickView::SizeRootObjectToView); + // setClearBeforeRendering(true); + setScreen(qApp->primaryScreen()); + setSource(QUrl(QStringLiteral("qrc:/qml/main.qml"))); + setVisible(true); + initSlideWindow(); + resizeWindow(); + onVisibilityChanged(); + + connect(qApp->primaryScreen(), &QScreen::virtualGeometryChanged, this, &MainWindow::resizeWindow); + connect(qApp->primaryScreen(), &QScreen::geometryChanged, this, &MainWindow::resizeWindow); + + connect(m_settings, &DockSettings::directionChanged, this, &MainWindow::onPositionChanged); + connect(m_settings, &DockSettings::iconSizeChanged, this, &MainWindow::onIconSizeChanged); + connect(m_settings, &DockSettings::visibilityChanged, this, &MainWindow::onVisibilityChanged); +} + +MainWindow::~MainWindow() +{ +} + +QRect MainWindow::windowRect() const +{ + const QRect screenGeometry = qApp->primaryScreen()->geometry(); + + QSize newSize(0, 0); + QPoint position(0, 0); + + switch (m_settings->direction()) { + case DockSettings::Left: + newSize = QSize(m_settings->iconSize(), screenGeometry.height() - m_settings->edgeMargins()); + position = { screenGeometry.x() + DockSettings::self()->edgeMargins() / 2, + (screenGeometry.height() - newSize.height()) / 2 + }; + break; + case DockSettings::Bottom: + newSize = QSize(screenGeometry.width() - DockSettings::self()->edgeMargins(), m_settings->iconSize()); + position = { (screenGeometry.width() - newSize.width()) / 2, + screenGeometry.y() + screenGeometry.height() - newSize.height() + - DockSettings::self()->edgeMargins() / 2 + }; + break; + default: + break; + } + + return QRect(position, newSize); +} + +void MainWindow::resizeWindow() +{ + setGeometry(windowRect()); + updateViewStruts(); + + emit resizingFished(); +} + +void MainWindow::initSlideWindow() +{ + KWindowEffects::SlideFromLocation location = KWindowEffects::NoEdge; + + if (m_settings->direction() == DockSettings::Left) + location = KWindowEffects::LeftEdge; + else if (m_settings->direction() == DockSettings::Bottom) + location = KWindowEffects::BottomEdge; + + KWindowEffects::slideWindow(winId(), location); +} + +void MainWindow::updateViewStruts() +{ + XWindowInterface::instance()->setViewStruts(this, m_settings->direction(), geometry()); +} + +void MainWindow::createFakeWindow() +{ + if (!m_fakeWindow) { + m_fakeWindow = new FakeWindow; + + connect(m_fakeWindow, &FakeWindow::containsMouseChanged, this, [=](bool contains) { + + }); + + connect(m_fakeWindow, &FakeWindow::dragEntered, this, [&] {}); + + } +} + +void MainWindow::deleteFakeWindow() +{ + if (m_fakeWindow) { + m_fakeWindow->deleteLater(); + m_fakeWindow = nullptr; + } +} + +void MainWindow::onPositionChanged() +{ + setVisible(false); + initSlideWindow(); + setVisible(true); + + setGeometry(windowRect()); + updateViewStruts(); + + emit positionChanged(); +} + +void MainWindow::onIconSizeChanged() +{ + setGeometry(windowRect()); + updateViewStruts(); + + emit iconSizeChanged(); +} + +void MainWindow::onVisibilityChanged() +{ + if (m_settings->visibility() == DockSettings::AlwaysVisible) + return; + + createFakeWindow(); +} diff --git a/src/mainwindow.h b/src/mainwindow.h new file mode 100644 index 0000000..5d986e3 --- /dev/null +++ b/src/mainwindow.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include + +#include "docksettings.h" +#include "applicationmodel.h" +#include "fakewindow.h" + +class MainWindow : public QQuickView +{ + Q_OBJECT + +public: + explicit MainWindow(QQuickView *parent = nullptr); + ~MainWindow(); + +signals: + void resizingFished(); + void iconSizeChanged(); + void positionChanged(); + +protected: + void event(QEvent *e, QObject *obj); + +private: + QRect windowRect() const; + void resizeWindow(); + void initSlideWindow(); + void updateViewStruts(); + + void createFakeWindow(); + void deleteFakeWindow(); + +private slots: + void onPositionChanged(); + void onIconSizeChanged(); + void onVisibilityChanged(); + +private: + DockSettings *m_settings; + ApplicationModel *m_appModel; + FakeWindow *m_fakeWindow; +}; + +#endif // MAINWINDOW_H diff --git a/src/processprovider.cpp b/src/processprovider.cpp new file mode 100644 index 0000000..90003b2 --- /dev/null +++ b/src/processprovider.cpp @@ -0,0 +1,16 @@ +#include "processprovider.h" +#include + +ProcessProvider::ProcessProvider(QObject *parent) + : QObject(parent) +{ + +} + +bool ProcessProvider::startDetached(const QString &exec, QStringList args) +{ + QProcess process; + process.setProgram(exec); + process.setArguments(args); + return process.startDetached(); +} diff --git a/src/processprovider.h b/src/processprovider.h new file mode 100644 index 0000000..8d07bc1 --- /dev/null +++ b/src/processprovider.h @@ -0,0 +1,16 @@ +#ifndef PROCESSPROVIDER_H +#define PROCESSPROVIDER_H + +#include + +class ProcessProvider : public QObject +{ + Q_OBJECT + +public: + explicit ProcessProvider(QObject *parent = nullptr); + + Q_INVOKABLE bool startDetached(const QString &exec, QStringList args = QStringList()); +}; + +#endif // PROCESSPROVIDER_H diff --git a/src/statusnotifier/dbustypes.cpp b/src/statusnotifier/dbustypes.cpp new file mode 100644 index 0000000..a1df260 --- /dev/null +++ b/src/statusnotifier/dbustypes.cpp @@ -0,0 +1,47 @@ +#include "dbustypes.h" + +// Marshall the IconPixmap data into a D-Bus argument +QDBusArgument &operator<<(QDBusArgument &argument, const IconPixmap &icon) +{ + argument.beginStructure(); + argument << icon.width; + argument << icon.height; + argument << icon.bytes; + argument.endStructure(); + return argument; +} + +// Retrieve the ImageStruct data from the D-Bus argument +const QDBusArgument &operator>>(const QDBusArgument &argument, IconPixmap &icon) +{ + argument.beginStructure(); + argument >> icon.width; + argument >> icon.height; + argument >> icon.bytes; + argument.endStructure(); + return argument; +} + +// Marshall the ToolTip data into a D-Bus argument +QDBusArgument &operator<<(QDBusArgument &argument, const ToolTip &toolTip) +{ + argument.beginStructure(); + argument << toolTip.iconName; + argument << toolTip.iconPixmap; + argument << toolTip.title; + argument << toolTip.description; + argument.endStructure(); + return argument; +} + +// Retrieve the ToolTip data from the D-Bus argument +const QDBusArgument &operator>>(const QDBusArgument &argument, ToolTip &toolTip) +{ + argument.beginStructure(); + argument >> toolTip.iconName; + argument >> toolTip.iconPixmap; + argument >> toolTip.title; + argument >> toolTip.description; + argument.endStructure(); + return argument; +} diff --git a/src/statusnotifier/dbustypes.h b/src/statusnotifier/dbustypes.h new file mode 100644 index 0000000..2a5809f --- /dev/null +++ b/src/statusnotifier/dbustypes.h @@ -0,0 +1,30 @@ +#include + +#ifndef DBUSTYPES_H +#define DBUSTYPES_H + +struct IconPixmap { + int width; + int height; + QByteArray bytes; +}; + +typedef QList IconPixmapList; + +struct ToolTip { + QString iconName; + QList iconPixmap; + QString title; + QString description; +}; + +QDBusArgument &operator<<(QDBusArgument &argument, const IconPixmap &icon); +const QDBusArgument &operator>>(const QDBusArgument &argument, IconPixmap &icon); + +QDBusArgument &operator<<(QDBusArgument &argument, const ToolTip &toolTip); +const QDBusArgument &operator>>(const QDBusArgument &argument, ToolTip &toolTip); + +Q_DECLARE_METATYPE(IconPixmap) +Q_DECLARE_METATYPE(ToolTip) + +#endif // DBUSTYPES_H diff --git a/src/statusnotifier/sniasync.cpp b/src/statusnotifier/sniasync.cpp new file mode 100644 index 0000000..92691eb --- /dev/null +++ b/src/statusnotifier/sniasync.cpp @@ -0,0 +1,21 @@ +#include "sniasync.h" + +SniAsync::SniAsync(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent/* = 0*/) + : QObject(parent) + , m_sni{service, path, connection} +{ + //forward StatusNotifierItem signals + connect(&m_sni, &org::kde::StatusNotifierItem::NewAttentionIcon, this, &SniAsync::NewAttentionIcon); + connect(&m_sni, &org::kde::StatusNotifierItem::NewIcon, this, &SniAsync::NewIcon); + connect(&m_sni, &org::kde::StatusNotifierItem::NewOverlayIcon, this, &SniAsync::NewOverlayIcon); + connect(&m_sni, &org::kde::StatusNotifierItem::NewStatus, this, &SniAsync::NewStatus); + connect(&m_sni, &org::kde::StatusNotifierItem::NewTitle, this, &SniAsync::NewTitle); + connect(&m_sni, &org::kde::StatusNotifierItem::NewToolTip, this, &SniAsync::NewToolTip); +} + +QDBusPendingReply SniAsync::asyncPropGet(QString const & property) +{ + QDBusMessage msg = QDBusMessage::createMethodCall(m_sni.service(), m_sni.path(), QLatin1String("org.freedesktop.DBus.Properties"), QLatin1String("Get")); + msg << m_sni.interface() << property; + return m_sni.connection().asyncCall(msg); +} diff --git a/src/statusnotifier/sniasync.h b/src/statusnotifier/sniasync.h new file mode 100644 index 0000000..848d7da --- /dev/null +++ b/src/statusnotifier/sniasync.h @@ -0,0 +1,89 @@ +#if !defined(SNIASYNC_H) +#define SNIASYNC_H + +#include +#include "statusnotifieriteminterface.h" + +template +struct remove_class_type { using type = void; }; // bluff +template +struct remove_class_type { using type = R(ArgTypes...); }; +template +struct remove_class_type { using type = R(ArgTypes...); }; + +template +class call_sig_helper +{ + template + static decltype(&L1::operator()) test(int); + template + static void test(...); //bluff +public: + using type = decltype(test(0)); +}; +template +struct call_signature : public remove_class_type::type> {}; +template +struct call_signature { using type = R (ArgTypes...); }; +template +struct call_signature { using type = R (ArgTypes...); }; +template +struct call_signature { using type = R (ArgTypes...); }; +template +struct call_signature { using type = R(ArgTypes...); }; + +template struct is_valid_signature : public std::false_type {}; +template +struct is_valid_signature : public std::true_type {}; + +class SniAsync : public QObject +{ + Q_OBJECT +public: + SniAsync(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = nullptr); + + template + inline void propertyGetAsync(QString const &name, F finished) + { + static_assert(is_valid_signature::type>::value, "need callable (lambda, *function, callable obj) (Arg) -> void"); + connect(new QDBusPendingCallWatcher{asyncPropGet(name), this}, + &QDBusPendingCallWatcher::finished, + [this, finished, name] (QDBusPendingCallWatcher * call) + { + QDBusPendingReply reply = *call; + if (reply.isError()) + qDebug().noquote().nospace() << "Error on DBus request(" << m_sni.service() << ',' << m_sni.path() << "): " << reply.error(); + finished(qdbus_cast::type>::argument_type>(reply.value())); + call->deleteLater(); + } + ); + } + + //exposed methods from org::kde::StatusNotifierItem + inline QString service() const { return m_sni.service(); } + +public slots: + //Forwarded slots from org::kde::StatusNotifierItem + inline QDBusPendingReply<> Activate(int x, int y) { return m_sni.Activate(x, y); } + inline QDBusPendingReply<> ContextMenu(int x, int y) { return m_sni.ContextMenu(x, y); } + inline QDBusPendingReply<> Scroll(int delta, const QString &orientation) { return m_sni.Scroll(delta, orientation); } + inline QDBusPendingReply<> SecondaryActivate(int x, int y) { return m_sni.SecondaryActivate(x, y); } + +signals: + //Forwarded signals from org::kde::StatusNotifierItem + void NewAttentionIcon(); + void NewIcon(); + void NewOverlayIcon(); + void NewStatus(const QString &status); + void NewTitle(); + void NewToolTip(); + +private: + QDBusPendingReply asyncPropGet(QString const & property); + +private: + org::kde::StatusNotifierItem m_sni; + +}; + +#endif diff --git a/src/statusnotifier/statusnotifieriteminterface.cpp b/src/statusnotifier/statusnotifieriteminterface.cpp new file mode 100644 index 0000000..65a18a9 --- /dev/null +++ b/src/statusnotifier/statusnotifieriteminterface.cpp @@ -0,0 +1,14 @@ +#include "statusnotifieriteminterface.h" + +/* + * Implementation of interface class StatusNotifierItemInterface + */ + +StatusNotifierItemInterface::StatusNotifierItemInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent) + : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) +{ +} + +StatusNotifierItemInterface::~StatusNotifierItemInterface() +{ +} diff --git a/src/statusnotifier/statusnotifieriteminterface.h b/src/statusnotifier/statusnotifieriteminterface.h new file mode 100644 index 0000000..baaa3cb --- /dev/null +++ b/src/statusnotifier/statusnotifieriteminterface.h @@ -0,0 +1,136 @@ +#ifndef STATUSNOTIFIERITEMINTERFACE_H +#define STATUSNOTIFIERITEMINTERFACE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include "dbustypes.h" + +/* + * Proxy class for interface org.kde.StatusNotifierItem + */ +class StatusNotifierItemInterface: public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char *staticInterfaceName() + { return "org.kde.StatusNotifierItem"; } + +public: + StatusNotifierItemInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = nullptr); + + ~StatusNotifierItemInterface(); + + Q_PROPERTY(QString AttentionIconName READ attentionIconName) + inline QString attentionIconName() const + { return qvariant_cast< QString >(property("AttentionIconName")); } + + Q_PROPERTY(IconPixmapList AttentionIconPixmap READ attentionIconPixmap) + inline IconPixmapList attentionIconPixmap() const + { return qvariant_cast< IconPixmapList >(property("AttentionIconPixmap")); } + + Q_PROPERTY(QString AttentionMovieName READ attentionMovieName) + inline QString attentionMovieName() const + { return qvariant_cast< QString >(property("AttentionMovieName")); } + + Q_PROPERTY(QString Category READ category) + inline QString category() const + { return qvariant_cast< QString >(property("Category")); } + + Q_PROPERTY(QString IconName READ iconName) + inline QString iconName() const + { return qvariant_cast< QString >(property("IconName")); } + + Q_PROPERTY(IconPixmapList IconPixmap READ iconPixmap) + inline IconPixmapList iconPixmap() const + { return qvariant_cast< IconPixmapList >(property("IconPixmap")); } + + Q_PROPERTY(QString IconThemePath READ iconThemePath) + inline QString iconThemePath() const + { return qvariant_cast< QString >(property("IconThemePath")); } + + Q_PROPERTY(QString Id READ id) + inline QString id() const + { return qvariant_cast< QString >(property("Id")); } + + Q_PROPERTY(bool ItemIsMenu READ itemIsMenu) + inline bool itemIsMenu() const + { return qvariant_cast< bool >(property("ItemIsMenu")); } + + Q_PROPERTY(QDBusObjectPath Menu READ menu) + inline QDBusObjectPath menu() const + { return qvariant_cast< QDBusObjectPath >(property("Menu")); } + + Q_PROPERTY(QString OverlayIconName READ overlayIconName) + inline QString overlayIconName() const + { return qvariant_cast< QString >(property("OverlayIconName")); } + + Q_PROPERTY(IconPixmapList OverlayIconPixmap READ overlayIconPixmap) + inline IconPixmapList overlayIconPixmap() const + { return qvariant_cast< IconPixmapList >(property("OverlayIconPixmap")); } + + Q_PROPERTY(QString Status READ status) + inline QString status() const + { return qvariant_cast< QString >(property("Status")); } + + Q_PROPERTY(QString Title READ title) + inline QString title() const + { return qvariant_cast< QString >(property("Title")); } + + Q_PROPERTY(ToolTip ToolTip READ toolTip) + inline ToolTip toolTip() const + { return qvariant_cast< ToolTip >(property("ToolTip")); } + + Q_PROPERTY(int WindowId READ windowId) + inline int windowId() const + { return qvariant_cast< int >(property("WindowId")); } + +public Q_SLOTS: // METHODS + inline QDBusPendingReply<> Activate(int x, int y) + { + QList argumentList; + argumentList << QVariant::fromValue(x) << QVariant::fromValue(y); + return asyncCallWithArgumentList(QLatin1String("Activate"), argumentList); + } + + inline QDBusPendingReply<> ContextMenu(int x, int y) + { + QList argumentList; + argumentList << QVariant::fromValue(x) << QVariant::fromValue(y); + return asyncCallWithArgumentList(QLatin1String("ContextMenu"), argumentList); + } + + inline QDBusPendingReply<> Scroll(int delta, const QString &orientation) + { + QList argumentList; + argumentList << QVariant::fromValue(delta) << QVariant::fromValue(orientation); + return asyncCallWithArgumentList(QLatin1String("Scroll"), argumentList); + } + + inline QDBusPendingReply<> SecondaryActivate(int x, int y) + { + QList argumentList; + argumentList << QVariant::fromValue(x) << QVariant::fromValue(y); + return asyncCallWithArgumentList(QLatin1String("SecondaryActivate"), argumentList); + } + +Q_SIGNALS: // SIGNALS + void NewAttentionIcon(); + void NewIcon(); + void NewOverlayIcon(); + void NewStatus(const QString &status); + void NewTitle(); + void NewToolTip(); +}; + +namespace org { + namespace kde { + typedef ::StatusNotifierItemInterface StatusNotifierItem; + } +} +#endif diff --git a/src/statusnotifier/statusnotifieritemsource.cpp b/src/statusnotifier/statusnotifieritemsource.cpp new file mode 100644 index 0000000..c2d46b1 --- /dev/null +++ b/src/statusnotifier/statusnotifieritemsource.cpp @@ -0,0 +1,211 @@ + #include "statusnotifieritemsource.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +class MenuImporter : public DBusMenuImporter +{ +public: + using DBusMenuImporter::DBusMenuImporter; + +protected: + virtual QIcon iconForName(const QString & name) override { + return QIcon::fromTheme(name); + } +}; + +StatusNotifierItemSource::StatusNotifierItemSource(const QString &id, QObject *parent) + : QObject(parent) + , m_id(id) + , m_refreshing(false) + , m_needsReRefreshing(false) + , m_menuImporter(nullptr) +{ + int slash = id.indexOf('/'); + if (slash == -1) { + qWarning() << "Invalid notifierItemId:" << id; + m_valid = false; + m_statusNotifierItemInterface = nullptr; + return; + } + + QString service = id.left(slash); + QString path = id.mid(slash); + + m_statusNotifierItemInterface = new org::kde::StatusNotifierItem(service, path, + QDBusConnection::sessionBus(), this); + m_refreshTimer.setSingleShot(true); + m_refreshTimer.setInterval(10); + connect(&m_refreshTimer, &QTimer::timeout, this, &StatusNotifierItemSource::performRefresh); + + m_valid = !service.isEmpty() && m_statusNotifierItemInterface->isValid(); + if (m_valid) { + connect(m_statusNotifierItemInterface, &StatusNotifierItemInterface::NewTitle, this, &StatusNotifierItemSource::refreshTitle); + connect(m_statusNotifierItemInterface, &StatusNotifierItemInterface::NewIcon, this, &StatusNotifierItemSource::refreshIcons); + connect(m_statusNotifierItemInterface, &StatusNotifierItemInterface::NewAttentionIcon, this, &StatusNotifierItemSource::refreshIcons); + connect(m_statusNotifierItemInterface, &StatusNotifierItemInterface::NewOverlayIcon, this, &StatusNotifierItemSource::refreshIcons); + connect(m_statusNotifierItemInterface, &StatusNotifierItemInterface::NewToolTip, this, &StatusNotifierItemSource::refreshToolTip); + refresh(); + } +} + +StatusNotifierItemSource::~StatusNotifierItemSource() +{ + delete m_statusNotifierItemInterface; +} + +void StatusNotifierItemSource::activate(int x, int y) +{ + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + m_statusNotifierItemInterface->Activate(x, y); + } +} + +void StatusNotifierItemSource::secondaryActivate(int x, int y) +{ + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + m_statusNotifierItemInterface->call(QDBus::NoBlock, QStringLiteral("SecondaryActivate"), x, y); + } +} + +void StatusNotifierItemSource::contextMenu(int x, int y) +{ + if (m_menuImporter) { + m_menuImporter->updateMenu(); + + // Popup menu + if (m_menuImporter->menu()) { + m_menuImporter->menu()->popup(QPoint(x, y)); + } + } else { + qWarning() << "Could not find DBusMenu interface, falling back to calling ContextMenu()"; + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + m_statusNotifierItemInterface->call(QDBus::NoBlock, QStringLiteral("ContextMenu"), x, y); + } + } +} + +void StatusNotifierItemSource::refresh() +{ + if (!m_refreshTimer.isActive()) { + m_refreshTimer.start(); + } +} + +void StatusNotifierItemSource::refreshTitle() +{ + refresh(); +} + +void StatusNotifierItemSource::refreshToolTip() +{ + refresh(); +} + +void StatusNotifierItemSource::refreshIcons() +{ + refresh(); +} + +void StatusNotifierItemSource::performRefresh() +{ + if (m_refreshing) { + m_needsReRefreshing = true; + return; + } + + m_refreshing = true; + QDBusMessage message = QDBusMessage::createMethodCall(m_statusNotifierItemInterface->service(), + m_statusNotifierItemInterface->path(), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("GetAll")); + + message << m_statusNotifierItemInterface->interface(); + QDBusPendingCall call = m_statusNotifierItemInterface->connection().asyncCall(message); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &StatusNotifierItemSource::refreshCallback); +} + +void StatusNotifierItemSource::refreshCallback(QDBusPendingCallWatcher *call) +{ + m_refreshing = false; + if (m_needsReRefreshing) { + m_needsReRefreshing = false; + performRefresh(); + call->deleteLater(); + return; + } + + QDBusPendingReply reply = *call; + if (reply.isError()) { + m_valid = false; + } else { + QVariantMap properties = reply.argumentAt<0>(); + QString path = properties[QStringLiteral("IconThemePath")].toString(); + + m_title = properties[QStringLiteral("Title")].toString(); + m_iconName = properties[QStringLiteral("IconName")].toString(); + + // ToolTip + ToolTip toolTip; + properties[QStringLiteral("ToolTip")].value() >> toolTip; + m_tooltip = toolTip.title; + + // Menu + QString menuObjectPath = properties[QStringLiteral("Menu")].value().path(); + if (!menuObjectPath.isEmpty()) { + if (menuObjectPath.startsWith(QLatin1String("/NO_DBUSMENU"))) { + qWarning() << "DBusMenu disabled for this application"; + } else { + m_menuImporter = new MenuImporter(m_statusNotifierItemInterface->service(), menuObjectPath, this); + } + } + + // Icon + IconPixmapList iconPixmaps; + properties[QStringLiteral("IconPixmap")].value() >> iconPixmaps; + + QImage image = IconPixmapListToImage(iconPixmaps); + if (!image.isNull()) { + QByteArray byteArray; + QBuffer buffer(&byteArray); + image.save(&buffer, "PNG"); + m_iconBytes = byteArray.toBase64(); + } + + emit updated(this); + } + + call->deleteLater(); +} + +QImage StatusNotifierItemSource::IconPixmapListToImage(const IconPixmapList &list) const +{ + QIcon icon; + + for (IconPixmap iconPixmap: list) { + if (!iconPixmap.bytes.isNull()) { + QImage image((uchar*) iconPixmap.bytes.data(), iconPixmap.width, + iconPixmap.height, QImage::Format_ARGB32); + + const uchar *end = image.constBits() + image.sizeInBytes(); + uchar *dest = reinterpret_cast(iconPixmap.bytes.data()); + for (const uchar *src = image.constBits(); src < end; src += 4, dest += 4) + qToUnaligned(qToBigEndian(qFromUnaligned(src)), dest); + + icon.addPixmap(QPixmap::fromImage(image)); + } + } + + return icon.pixmap(QSize(24, 24)).toImage(); +} diff --git a/src/statusnotifier/statusnotifieritemsource.h b/src/statusnotifier/statusnotifieritemsource.h new file mode 100644 index 0000000..a5cdf20 --- /dev/null +++ b/src/statusnotifier/statusnotifieritemsource.h @@ -0,0 +1,57 @@ +#ifndef STATUSNOTIFIERITEMSOURCE_H +#define STATUSNOTIFIERITEMSOURCE_H + +#include +#include + +#include "statusnotifieriteminterface.h" + +class MenuImporter; +class StatusNotifierItemSource : public QObject +{ + Q_OBJECT + +public: + explicit StatusNotifierItemSource(const QString &id, QObject *parent = nullptr); + ~StatusNotifierItemSource(); + + QString id() const { return m_id; } + QString title() const { return m_title; } + QString tooltip() const { return m_tooltip; } + QString iconName() const { return m_iconName; } + QString iconBytes() const { return m_iconBytes; } + + void activate(int x, int y); + void secondaryActivate(int x, int y); + void contextMenu(int x, int y); + + MenuImporter *menuImporter() { return m_menuImporter; } + +signals: + void updated(StatusNotifierItemSource *); + +private slots: + void refresh(); + void refreshTitle(); + void refreshToolTip(); + void refreshIcons(); + void performRefresh(); + void refreshCallback(QDBusPendingCallWatcher *); + QImage IconPixmapListToImage(const IconPixmapList &list) const; + +private: + QString m_id; + QString m_title; + QString m_tooltip; + QString m_iconName; + QString m_iconBytes; + + bool m_valid; + bool m_refreshing; + bool m_needsReRefreshing; + StatusNotifierItemInterface *m_statusNotifierItemInterface; + MenuImporter *m_menuImporter; + QTimer m_refreshTimer; +}; + +#endif // STATUSNOTIFIERITEMSOURCE_H diff --git a/src/statusnotifier/statusnotifiermodel.cpp b/src/statusnotifier/statusnotifiermodel.cpp new file mode 100644 index 0000000..fe84b99 --- /dev/null +++ b/src/statusnotifier/statusnotifiermodel.cpp @@ -0,0 +1,162 @@ +#include "statusnotifiermodel.h" +#include +#include +#include +#include + +#include +#include +#include +#include + +StatusNotifierModel::StatusNotifierModel(QObject *parent) + : QAbstractListModel(parent) + , m_watcher(nullptr) +{ + QFutureWatcher * futureWatcher = new QFutureWatcher; + connect(futureWatcher, &QFutureWatcher::finished, this, [this, futureWatcher] { + m_watcher = futureWatcher->future().result(); + + connect(m_watcher, &StatusNotifierWatcher::StatusNotifierItemRegistered, + this, &StatusNotifierModel::itemAdded); + connect(m_watcher, &StatusNotifierWatcher::StatusNotifierItemUnregistered, + this, &StatusNotifierModel::itemRemoved); + + qDebug() << m_watcher->RegisteredStatusNotifierItems(); + + futureWatcher->deleteLater(); + }); + + QFuture future = QtConcurrent::run([=] { + QString dbusName = QStringLiteral("org.kde.StatusNotifierHost-%1-%2").arg(QApplication::applicationPid()).arg(1); + if (QDBusConnectionInterface::ServiceNotRegistered == QDBusConnection::sessionBus().interface()->registerService(dbusName, QDBusConnectionInterface::DontQueueService)) + qDebug() << "unable to register service for " << dbusName; + + StatusNotifierWatcher * watcher = new StatusNotifierWatcher; + watcher->RegisterStatusNotifierHost(dbusName); + watcher->moveToThread(QApplication::instance()->thread()); + return watcher; + }); + + futureWatcher->setFuture(future); +} + +StatusNotifierModel::~StatusNotifierModel() +{ + delete m_watcher; +} + +int StatusNotifierModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return m_items.size(); +} + +QHash StatusNotifierModel::roleNames() const +{ + QHash roles; + roles[IdRole] = "id"; + roles[IconNameRole] = "iconName"; + roles[IconBytesRole] = "iconBytes"; + roles[TitleRole] = "title"; + roles[ToolTipRole] = "toolTip"; + return roles; +} + +QVariant StatusNotifierModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + StatusNotifierItemSource *item = m_items.at(index.row()); + + switch (role) { + case IdRole: + return item->id(); + case IconNameRole: + return item->iconName(); + case IconBytesRole: + return item->iconBytes(); + case TitleRole: + return item->title(); + case ToolTipRole: + return item->tooltip(); + } + + return QVariant(); +} + +int StatusNotifierModel::indexOf(const QString &id) +{ + for (StatusNotifierItemSource *item : m_items) { + if (item->id() == id) + return m_items.indexOf(item); + } + + return -1; +} + +StatusNotifierItemSource *StatusNotifierModel::findItemById(const QString &id) +{ + int index = indexOf(id); + + if (index == -1) + return nullptr; + + return m_items.at(index); +} + +void StatusNotifierModel::leftButtonClick(const QString &id) +{ + StatusNotifierItemSource *item = findItemById(id); + if (item) { + QPoint p(QCursor::pos()); + item->activate(p.x(), p.y()); + } +} + +void StatusNotifierModel::rightButtonClick(const QString &id) +{ + StatusNotifierItemSource *item = findItemById(id); + if (item) { + QPoint p(QCursor::pos()); + item->contextMenu(p.x(), p.y()); + } +} + +void StatusNotifierModel::itemAdded(QString serviceAndPath) +{ + StatusNotifierItemSource *source = new StatusNotifierItemSource(serviceAndPath, this); + + connect(source, &StatusNotifierItemSource::updated, this, &StatusNotifierModel::updated); + + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + m_items.append(source); + endInsertRows(); +} + +void StatusNotifierModel::itemRemoved(const QString &serviceAndPath) +{ + int index = indexOf(serviceAndPath); + + if (index != -1) { + beginRemoveRows(QModelIndex(), index, index); + StatusNotifierItemSource *item = m_items.at(index); + m_items.removeAll(item); + endRemoveRows(); + } +} + +void StatusNotifierModel::updated(StatusNotifierItemSource *item) +{ + if (!item) + return; + + int idx = indexOf(item->id()); + + // update + if (idx != -1) { + dataChanged(index(idx, 0), index(idx, 0)); + } +} diff --git a/src/statusnotifier/statusnotifiermodel.h b/src/statusnotifier/statusnotifiermodel.h new file mode 100644 index 0000000..40245e1 --- /dev/null +++ b/src/statusnotifier/statusnotifiermodel.h @@ -0,0 +1,53 @@ +#ifndef STATUSNOTIFIERMODEL_H +#define STATUSNOTIFIERMODEL_H + +#include +#include +#include + +#include + +#include "statusnotifieritemsource.h" +#include "statusnotifierwatcher.h" +#include "sniasync.h" + +class StatusNotifierModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + IdRole = Qt::UserRole + 1, + IconNameRole, + IconBytesRole, + IconRole, + TitleRole, + ToolTipRole + }; + + explicit StatusNotifierModel(QObject *parent = nullptr); + ~StatusNotifierModel(); + + // Basic functionality: + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + int indexOf(const QString &id); + StatusNotifierItemSource *findItemById(const QString &id); + + Q_INVOKABLE void leftButtonClick(const QString &id); + Q_INVOKABLE void rightButtonClick(const QString &id); + +public slots: + void itemAdded(QString serviceAndPath); + void itemRemoved(const QString &serviceAndPath); + void updated(StatusNotifierItemSource *item); + +private: + StatusNotifierWatcher *m_watcher; + QList m_items; +}; + +#endif // STATUSNOTIFIERMODEL_H diff --git a/src/statusnotifier/statusnotifierwatcher.cpp b/src/statusnotifier/statusnotifierwatcher.cpp new file mode 100644 index 0000000..d8b5189 --- /dev/null +++ b/src/statusnotifier/statusnotifierwatcher.cpp @@ -0,0 +1,99 @@ +#include "statusnotifierwatcher.h" +#include +#include + +StatusNotifierWatcher::StatusNotifierWatcher(QObject *parent) : QObject(parent) +{ + qRegisterMetaType("IconPixmap"); + qDBusRegisterMetaType(); + qRegisterMetaType("IconPixmapList"); + qDBusRegisterMetaType(); + qRegisterMetaType("ToolTip"); + qDBusRegisterMetaType(); + + QDBusConnection dbus = QDBusConnection::sessionBus(); + switch (dbus.interface()->registerService(QStringLiteral("org.kde.StatusNotifierWatcher"), QDBusConnectionInterface::QueueService).value()) + { + case QDBusConnectionInterface::ServiceNotRegistered: + qWarning() << "StatusNotifier: unable to register service for org.kde.StatusNotifierWatcher"; + break; + case QDBusConnectionInterface::ServiceQueued: + qWarning() << "StatusNotifier: registration of service org.kde.StatusNotifierWatcher queued, we can become primary after existing one deregisters"; + break; + case QDBusConnectionInterface::ServiceRegistered: + break; + } + + dbus.registerObject(QStringLiteral("/StatusNotifierWatcher"), this, QDBusConnection::ExportScriptableContents); + +// if (!dbus.registerObject(QStringLiteral("/StatusNotifierWatcher"), this, QDBusConnection::ExportScriptableContents)) +// qDebug() << QDBusConnection::sessionBus().lastError().message(); + + mWatcher = new QDBusServiceWatcher(this); + mWatcher->setConnection(dbus); + mWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + + connect(mWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &StatusNotifierWatcher::serviceUnregistered); +} + +StatusNotifierWatcher::~StatusNotifierWatcher() +{ + QDBusConnection::sessionBus().unregisterService(QStringLiteral("org.kde.StatusNotifierWatcher")); +} + +void StatusNotifierWatcher::RegisterStatusNotifierItem(const QString &serviceOrPath) +{ + QString service = serviceOrPath; + QString path = QStringLiteral("/StatusNotifierItem"); + + // workaround for sni-qt + if (service.startsWith(QLatin1Char('/'))) { + path = service; + service = message().service(); + } + + QString notifierItemId = service + path; + + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(service).value() + && !mServices.contains(notifierItemId)) { + mServices << notifierItemId; + mWatcher->addWatchedService(service); + emit StatusNotifierItemRegistered(notifierItemId); + } +} + +void StatusNotifierWatcher::RegisterStatusNotifierHost(const QString &service) +{ + if (!mHosts.contains(service)) + { + mHosts.append(service); + mWatcher->addWatchedService(service); + } +} + +void StatusNotifierWatcher::serviceUnregistered(const QString &service) +{ + // qDebug() << "Service" << service << "unregistered"; + + mWatcher->removeWatchedService(service); + + if (mHosts.contains(service)) + { + mHosts.removeAll(service); + return; + } + + QString match = service + QLatin1Char('/'); + QStringList::Iterator it = mServices.begin(); + while (it != mServices.end()) + { + if (it->startsWith(match)) + { + QString name = *it; + it = mServices.erase(it); + emit StatusNotifierItemUnregistered(name); + } + else + ++it; + } +} diff --git a/src/statusnotifier/statusnotifierwatcher.h b/src/statusnotifier/statusnotifierwatcher.h new file mode 100644 index 0000000..a4d2f98 --- /dev/null +++ b/src/statusnotifier/statusnotifierwatcher.h @@ -0,0 +1,45 @@ +#ifndef STATUSNOTIFIERWATCHER_H +#define STATUSNOTIFIERWATCHER_H + +#include +#include +#include +#include +#include + +#include "dbustypes.h" + +class StatusNotifierWatcher : public QObject, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.StatusNotifierWatcher") + Q_SCRIPTABLE Q_PROPERTY(bool IsStatusNotifierHostRegistered READ isStatusNotifierHostRegistered) + Q_SCRIPTABLE Q_PROPERTY(int ProtocolVersion READ protocolVersion) + Q_SCRIPTABLE Q_PROPERTY(QStringList RegisteredStatusNotifierItems READ RegisteredStatusNotifierItems) + +public: + explicit StatusNotifierWatcher(QObject *parent = nullptr); + ~StatusNotifierWatcher(); + + bool isStatusNotifierHostRegistered() { return mHosts.count() > 0; } + int protocolVersion() const { return 0; } + QStringList RegisteredStatusNotifierItems() const { return mServices; } + +signals: + Q_SCRIPTABLE void StatusNotifierItemRegistered(const QString &service); + Q_SCRIPTABLE void StatusNotifierItemUnregistered(const QString &service); + Q_SCRIPTABLE void StatusNotifierHostRegistered(); + +public slots: + Q_SCRIPTABLE void RegisterStatusNotifierItem(const QString &serviceOrPath); + Q_SCRIPTABLE void RegisterStatusNotifierHost(const QString &service); + + void serviceUnregistered(const QString &service); + +private: + QStringList mServices; + QStringList mHosts; + QDBusServiceWatcher *mWatcher; +}; + +#endif // STATUSNOTIFIERWATCHER_H diff --git a/src/systemappitem.cpp b/src/systemappitem.cpp new file mode 100644 index 0000000..dc2af7a --- /dev/null +++ b/src/systemappitem.cpp @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "systemappitem.h" + +SystemAppItem::SystemAppItem(QObject *parent) + : QObject(parent) +{ + +} diff --git a/src/systemappitem.h b/src/systemappitem.h new file mode 100644 index 0000000..94521d1 --- /dev/null +++ b/src/systemappitem.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SYSTEMAPPITEM_H +#define SYSTEMAPPITEM_H + +#include + +class SystemAppItem : public QObject +{ + Q_OBJECT + +public: + explicit SystemAppItem(QObject *parent = nullptr); + + QString path; + QString name; + QString genericName; + QString comment; + QString iconName; + QString startupWMClass; + QString exec; + QStringList args; +}; + +#endif // SYSTEMAPPITEM_H diff --git a/src/systemappmonitor.cpp b/src/systemappmonitor.cpp new file mode 100644 index 0000000..22bb9d4 --- /dev/null +++ b/src/systemappmonitor.cpp @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "systemappmonitor.h" + +#include +#include +#include +#include +#include + +#define SystemApplicationsFolder "/usr/share/applications" + +static SystemAppMonitor *SELF = nullptr; + +static QByteArray detectDesktopEnvironment() +{ + const QByteArray desktop = qgetenv("XDG_CURRENT_DESKTOP"); + + if (!desktop.isEmpty()) + return desktop.toUpper(); + + return QByteArray("UNKNOWN"); +} + +SystemAppMonitor *SystemAppMonitor::self() +{ + if (SELF == nullptr) + SELF = new SystemAppMonitor; + + return SELF; +} + +SystemAppMonitor::SystemAppMonitor(QObject *parent) + : QObject(parent) +{ + QFileSystemWatcher *watcher = new QFileSystemWatcher(this); + watcher->addPath(SystemApplicationsFolder); + connect(watcher, &QFileSystemWatcher::directoryChanged, this, &SystemAppMonitor::refresh); + refresh(); +} + +SystemAppMonitor::~SystemAppMonitor() +{ + while (!m_items.isEmpty()) + delete m_items.takeFirst(); +} + +SystemAppItem *SystemAppMonitor::find(const QString &filePath) +{ + for (SystemAppItem *item : m_items) + if (item->path == filePath) + return item; + + return nullptr; +} + +void SystemAppMonitor::refresh() +{ + QStringList addedEntries; + for (SystemAppItem *item : m_items) + addedEntries.append(item->path); + + QStringList allEntries; + QDirIterator it(SystemApplicationsFolder, { "*.desktop" }, QDir::NoFilter, QDirIterator::Subdirectories); + + while (it.hasNext()) { + const QString &filePath = it.next(); + + if (!QFile::exists(filePath)) + continue; + + allEntries.append(filePath); + } + + for (const QString &filePath : allEntries) { + if (!addedEntries.contains(filePath)) { + addApplication(filePath); + } + } + + for (SystemAppItem *item : m_items) { + if (!allEntries.contains(item->path)) { + removeApplication(item); + } + } + + emit refreshed(); +} + +void SystemAppMonitor::addApplication(const QString &filePath) +{ + if (find(filePath)) + return; + + QSettings desktop(filePath, QSettings::IniFormat); + desktop.setIniCodec("UTF-8"); + desktop.beginGroup("Desktop Entry"); + + if (desktop.contains("OnlyShowIn")) { + const QString &value = desktop.value("OnlyShowIn").toString(); + if (!value.contains(detectDesktopEnvironment(), Qt::CaseInsensitive)) { + return; + } + } + + if (desktop.value("NoDisplay").toBool() || + desktop.value("Hidden").toBool()) { + return; + } + + QString appName = desktop.value(QString("Name[%1]").arg(QLocale::system().name())).toString(); + QString appExec = desktop.value("Exec").toString(); + + if (appName.isEmpty()) + appName = desktop.value("Name").toString(); + + appExec.remove(QRegularExpression("%.")); + appExec.remove(QRegularExpression("^\"")); + // appExec.remove(QRegularExpression(" *$")); + appExec = appExec.simplified(); + + SystemAppItem *item = new SystemAppItem; + item->path = filePath; + item->name = appName; + item->genericName = desktop.value("GenericName").toString(); + item->comment = desktop.value("Comment").toString(); + item->iconName = desktop.value("Icon").toString(); + item->startupWMClass = desktop.value("StartupWMClass").toString(); + item->exec = appExec; + item->args = appExec.split(" "); + + m_items.append(item); +} + +void SystemAppMonitor::removeApplication(SystemAppItem *item) +{ + m_items.removeOne(item); + item->deleteLater(); +} diff --git a/src/systemappmonitor.h b/src/systemappmonitor.h new file mode 100644 index 0000000..c3188f5 --- /dev/null +++ b/src/systemappmonitor.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SYSTEMAPPMONITOR_H +#define SYSTEMAPPMONITOR_H + +#include +#include "systemappitem.h" + +class SystemAppMonitor : public QObject +{ + Q_OBJECT + +public: + static SystemAppMonitor *self(); + + explicit SystemAppMonitor(QObject *parent = nullptr); + ~SystemAppMonitor(); + + SystemAppItem *find(const QString &filePath); + QList applications() { return m_items; } + +signals: + void refreshed(); + +private: + void refresh(); + void addApplication(const QString &filePath); + void removeApplication(SystemAppItem *item); + +private: + QList m_items; +}; + +#endif // SYSTEMAPPMONITOR_H diff --git a/src/trashmanager.cpp b/src/trashmanager.cpp new file mode 100644 index 0000000..764a5c3 --- /dev/null +++ b/src/trashmanager.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "trashmanager.h" +#include +#include + +const QString TrashDir = QDir::homePath() + "/.local/share/Trash"; +const QDir::Filters ItemsShouldCount = QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot; + +TrashManager::TrashManager(QObject *parent) + : QObject(parent), + m_filesWatcher(new QFileSystemWatcher(this)), + m_count(0) +{ + onDirectoryChanged(); + connect(m_filesWatcher, &QFileSystemWatcher::directoryChanged, this, &TrashManager::onDirectoryChanged, Qt::QueuedConnection); +} + +void TrashManager::emptyTrash() +{ + +} + +void TrashManager::openTrash() +{ + QProcess::startDetached("gio", QStringList() << "open" << "trash:///"); +} + +void TrashManager::onDirectoryChanged() +{ + m_filesWatcher->addPath(TrashDir); + + if (QDir(TrashDir + "/files").exists()) { + m_filesWatcher->addPath(TrashDir + "/files"); + m_count = QDir(TrashDir + "/files").entryList(ItemsShouldCount).count(); + } else { + m_count = 0; + } + + emit countChanged(); +} diff --git a/src/trashmanager.h b/src/trashmanager.h new file mode 100644 index 0000000..9e56da4 --- /dev/null +++ b/src/trashmanager.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TRASHMANAGER_H +#define TRASHMANAGER_H + +#include +#include + +class TrashManager : public QObject +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + explicit TrashManager(QObject *parent = nullptr); + + Q_INVOKABLE void openTrash(); + Q_INVOKABLE void emptyTrash(); + + int count() { return m_count; } + +Q_SIGNALS: + void countChanged(); + +private slots: + void onDirectoryChanged(); + +private: + QFileSystemWatcher *m_filesWatcher; + int m_count; +}; + +#endif // TRASHMANAGER_H diff --git a/src/utils.cpp b/src/utils.cpp new file mode 100644 index 0000000..6471a0e --- /dev/null +++ b/src/utils.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "utils.h" +#include "systemappmonitor.h" +#include "systemappitem.h" + +#include +#include +#include +#include +#include + +#include + +static Utils *INSTANCE = nullptr; + +Utils *Utils::instance() +{ + if (!INSTANCE) + INSTANCE = new Utils; + + return INSTANCE; +} + +Utils::Utils(QObject *parent) + : QObject(parent) + , m_sysAppMonitor(SystemAppMonitor::self()) +{ + +} + +QString Utils::cmdFromPid(quint32 pid) +{ + QFile file(QString("/proc/%1/cmdline").arg(pid)); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return QString(); + + QString cmd = QString::fromUtf8(file.readAll()); + QString bin; + + for (int i = 0; i < cmd.size(); ++i) { + const QChar ch = cmd[i]; + if (ch == '\\') + i++; + else if (ch == ' ') + break; + else + bin += ch; + } + + if (bin.split("/").last() == "electron") + return QString(); + + if (bin.startsWith("/")) + return bin.split("/").last(); + + return bin; +} + +QString Utils::desktopPathFromMetadata(const QString &appId, quint32 pid, const QString &xWindowWMClassName) +{ + QString result; + + if (!appId.isEmpty() && !xWindowWMClassName.isEmpty()) { + for (SystemAppItem *item : m_sysAppMonitor->applications()) { + // Start search. + const QFileInfo desktopFileInfo(item->path); + const QString cmdline = cmdFromPid(pid); + + bool founded = false; + + if (desktopFileInfo.baseName() == xWindowWMClassName || + desktopFileInfo.completeBaseName() == xWindowWMClassName) + founded = true; + + // StartupWMClass=STRING + // If true, it is KNOWN that the application will map at least one + // window with the given string as its WM class or WM name hint. + // ref: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-0.1.txt + if (item->startupWMClass.startsWith(appId, Qt::CaseInsensitive) || + item->startupWMClass.startsWith(xWindowWMClassName, Qt::CaseInsensitive)) + founded = true; + + if (!founded && item->iconName.startsWith(xWindowWMClassName, Qt::CaseInsensitive)) + founded = true; + + // Icon name and cmdline. + if (!founded && item->iconName == cmdline) + founded = true; + + // Exec name and cmdline. + if (!founded && item->exec == cmdline) + founded = true; + + // Try matching mapped name against 'Name'. + if (!founded && item->name.startsWith(xWindowWMClassName, Qt::CaseInsensitive)) + founded = true; + + // exec + if (!founded && item->exec.startsWith(xWindowWMClassName, Qt::CaseInsensitive)) + founded = true; + + if (!founded && desktopFileInfo.baseName().startsWith(xWindowWMClassName, Qt::CaseInsensitive)) + founded = true; + + if (founded) { + result = item->path; + break; + } + } + } + + return result; +} + +QMap Utils::readInfoFromDesktop(const QString &desktopFile) +{ + QMap info; + for (SystemAppItem *item : m_sysAppMonitor->applications()) { + if (item->path == desktopFile) { + info.insert("Icon", item->iconName); + info.insert("Name", item->name); + info.insert("Exec", item->exec); + } + } + + return info; +} diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..e4a99ed --- /dev/null +++ b/src/utils.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef UTILS_H +#define UTILS_H + +#include +#include +#include + +class SystemAppMonitor; +class Utils : public QObject +{ + Q_OBJECT + +public: + struct AppData + { + QString id; // Application id (*.desktop sans extension). + QString name; // Application name. + QString genericName; // Generic application name. + QIcon icon; + QUrl url; + bool skipTaskbar = false; + }; + + enum UrlComparisonMode { + Strict = 0, + IgnoreQueryItems + }; + + static Utils *instance(); + + explicit Utils(QObject *parent = nullptr); + + QString cmdFromPid(quint32 pid); + QString desktopPathFromMetadata(const QString &appId, quint32 pid = 0, + const QString &xWindowWMClassName = QString()); + QMap readInfoFromDesktop(const QString &desktopFile); + +private: + SystemAppMonitor *m_sysAppMonitor; +}; + +#endif // UTILS_H diff --git a/src/volumemanager.cpp b/src/volumemanager.cpp new file mode 100644 index 0000000..8cdffcb --- /dev/null +++ b/src/volumemanager.cpp @@ -0,0 +1,151 @@ +#include "volumemanager.h" + +#include +#include +#include +#include +#include + +static const QString Service = "org.cyber.Settings"; +static const QString ObjectPath = "/Audio"; +static const QString Interface = "org.cyber.Audio"; + +static VolumeManager *SELF = nullptr; + +VolumeManager *VolumeManager::self() +{ + if (!SELF) + SELF = new VolumeManager; + + return SELF; +} + +VolumeManager::VolumeManager(QObject *parent) + : QObject(parent) + , m_isValid(false) + , m_isMute(false) + , m_volume(0) +{ + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(this); + watcher->setConnection(QDBusConnection::sessionBus()); + watcher->addWatchedService(Service); + + init(); + + connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &VolumeManager::init); +} + +bool VolumeManager::isValid() const +{ + return m_isValid; +} + +void VolumeManager::initStatus() +{ + QDBusInterface iface(Service, ObjectPath, Interface, QDBusConnection::sessionBus(), this); + + m_isValid = iface.isValid() && !iface.lastError().isValid(); + + if (m_isValid) { + int volume = iface.property("volume").toInt(); + bool mute = iface.property("mute").toBool(); + + if (m_volume != volume) { + m_volume = volume; + emit volumeChanged(); + } + + if (m_isMute != mute) { + m_isMute = mute; + emit muteChanged(); + } + } + + emit validChanged(); +} + +void VolumeManager::connectDBusSignals() +{ + QDBusInterface iface(Service, ObjectPath, Interface, QDBusConnection::sessionBus(), this); + + if (iface.isValid()) { + QDBusConnection::sessionBus().connect(Service, ObjectPath, Interface, "volumeChanged", + this, SLOT(onDBusVolumeChanged(int))); + QDBusConnection::sessionBus().connect(Service, ObjectPath, Interface, "muteChanged", + this, SLOT(onDBusMuteChanged(bool))); + } +} + +void VolumeManager::onDBusVolumeChanged(int volume) +{ + if (m_volume != volume) { + m_volume = volume; + emit volumeChanged(); + } +} + +void VolumeManager::onDBusMuteChanged(bool mute) +{ + if (m_isMute != mute) { + m_isMute = mute; + emit muteChanged(); + + // Need to update the icon. + emit volumeChanged(); + } +} + +int VolumeManager::volume() const +{ + return m_volume; +} + +QString VolumeManager::iconName() const +{ + if (m_volume <= 0 || m_isMute) + return QStringLiteral("audio-volume-muted-symbolic"); + else if (m_volume <= 25) + return QStringLiteral("audio-volume-low-symbolic"); + else if (m_volume <= 75) + return QStringLiteral("audio-volume-medium-symbolic"); + else + return QStringLiteral("audio-volume-high-symbolic"); +} + +void VolumeManager::toggleMute() +{ + QDBusInterface iface(Service, ObjectPath, Interface, QDBusConnection::sessionBus(), this); + + if (iface.isValid()) { + iface.call("toggleMute"); + } +} + +void VolumeManager::setMute(bool mute) +{ + QDBusInterface iface(Service, ObjectPath, Interface, QDBusConnection::sessionBus(), this); + + if (iface.isValid()) { + iface.call("setMute", QVariant::fromValue(mute)); + } +} + +void VolumeManager::setVolume(int value) +{ + QDBusInterface iface(Service, ObjectPath, Interface, QDBusConnection::sessionBus(), this); + + if (iface.isValid()) { + iface.call("setVolume", QVariant::fromValue(value)); + } +} + +void VolumeManager::init() +{ + initStatus(); + connectDBusSignals(); +} + +bool VolumeManager::isMute() const +{ + return m_isMute; +} diff --git a/src/volumemanager.h b/src/volumemanager.h new file mode 100644 index 0000000..f1e31f1 --- /dev/null +++ b/src/volumemanager.h @@ -0,0 +1,49 @@ +#ifndef VOLUMEMANAGER_H +#define VOLUMEMANAGER_H + +#include + +class VolumeManager : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool isValid READ isValid NOTIFY validChanged) + Q_PROPERTY(bool isMute READ isMute NOTIFY muteChanged) + Q_PROPERTY(int volume READ volume NOTIFY volumeChanged) + Q_PROPERTY(QString iconName READ iconName NOTIFY volumeChanged) + +public: + static VolumeManager *self(); + + explicit VolumeManager(QObject *parent = nullptr); + + bool isValid() const; + bool isMute() const; + int volume() const; + + QString iconName() const; + + Q_INVOKABLE void toggleMute(); + Q_INVOKABLE void setMute(bool mute); + Q_INVOKABLE void setVolume(int value); + +signals: + void validChanged(); + void muteChanged(); + void volumeChanged(); + +private: + void init(); + void initStatus(); + void connectDBusSignals(); + +private slots: + void onDBusVolumeChanged(int volume); + void onDBusMuteChanged(bool mute); + +private: + bool m_isValid; + bool m_isMute; + int m_volume; +}; + +#endif // VOLUMEMANAGER_H diff --git a/src/xwindowinterface.cpp b/src/xwindowinterface.cpp new file mode 100644 index 0000000..a0141c1 --- /dev/null +++ b/src/xwindowinterface.cpp @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "xwindowinterface.h" +#include "utils.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +// X11 +#include + +static XWindowInterface *INSTANCE = nullptr; + +XWindowInterface *XWindowInterface::instance() +{ + if (!INSTANCE) + INSTANCE = new XWindowInterface; + + return INSTANCE; +} + +XWindowInterface::XWindowInterface(QObject *parent) + : QObject(parent) +{ + connect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &XWindowInterface::onWindowadded); + connect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &XWindowInterface::windowRemoved); + connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &XWindowInterface::activeChanged); +} + +void XWindowInterface::enableBlurBehind(QWindow *view, bool enable, const QRegion ®ion) +{ + KWindowEffects::enableBlurBehind(view->winId(), enable, region); +} + +WId XWindowInterface::activeWindow() +{ + return KWindowSystem::activeWindow(); +} + +void XWindowInterface::minimizeWindow(WId win) +{ + KWindowSystem::minimizeWindow(win); +} + +void XWindowInterface::closeWindow(WId id) +{ + // FIXME: Why there is no such thing in KWindowSystem?? + NETRootInfo(QX11Info::connection(), NET::CloseWindow).closeWindowRequest(id); +} + +void XWindowInterface::forceActiveWindow(WId win) +{ + KWindowSystem::forceActiveWindow(win); +} + +QMap XWindowInterface::requestInfo(quint64 wid) +{ + const KWindowInfo winfo { wid, NET::WMFrameExtents + | NET::WMWindowType + | NET::WMGeometry + | NET::WMDesktop + | NET::WMState + | NET::WMName + | NET::WMVisibleName, + NET::WM2WindowClass + | NET::WM2Activities + | NET::WM2AllowedActions + | NET::WM2TransientFor }; + QMap result; + const QString winClass = QString(winfo.windowClassClass()); + + result.insert("iconName", winClass.toLower()); + result.insert("active", wid == KWindowSystem::activeWindow()); + result.insert("visibleName", winfo.visibleName()); + result.insert("id", winClass); + + return result; +} + +QString XWindowInterface::requestWindowClass(quint64 wid) +{ + return KWindowInfo(wid, NET::Supported, NET::WM2WindowClass).windowClassClass(); +} + +bool XWindowInterface::isAcceptableWindow(quint64 wid) +{ + QFlags ignoreList; + ignoreList |= NET::DesktopMask; + ignoreList |= NET::DockMask; + ignoreList |= NET::SplashMask; + ignoreList |= NET::ToolbarMask; + ignoreList |= NET::MenuMask; + ignoreList |= NET::PopupMenuMask; + ignoreList |= NET::NotificationMask; + + KWindowInfo info(wid, NET::WMWindowType | NET::WMState, NET::WM2TransientFor | NET::WM2WindowClass); + + if (!info.valid()) + return false; + + if (NET::typeMatchesMask(info.windowType(NET::AllTypesMask), ignoreList)) + return false; + + if (info.hasState(NET::SkipTaskbar) || info.hasState(NET::SkipPager)) + return false; + + // WM_TRANSIENT_FOR hint not set - normal window + WId transFor = info.transientFor(); + if (transFor == 0 || transFor == wid || transFor == (WId) QX11Info::appRootWindow()) + return true; + + info = KWindowInfo(transFor, NET::WMWindowType); + + QFlags normalFlag; + normalFlag |= NET::NormalMask; + normalFlag |= NET::DialogMask; + normalFlag |= NET::UtilityMask; + + return !NET::typeMatchesMask(info.windowType(NET::AllTypesMask), normalFlag); +} + +void XWindowInterface::setViewStruts(QWindow *view, DockSettings::Direction direction, const QRect &rect, bool isMax) +{ + NETExtendedStrut strut; + + const auto screen = view->screen(); + + const QRect currentScreen { screen->geometry() }; + const QRect wholeScreen { {0, 0}, screen->virtualSize() }; + + switch (direction) { + case DockSettings::Left: { + const int leftOffset = { screen->geometry().left() }; + strut.left_width = rect.width() + leftOffset + DockSettings::self()->edgeMargins(); + strut.left_start = rect.y(); + strut.left_end = rect.y() + rect.height() - 1; + break; + } + case DockSettings::Bottom: { + const int bottomOffset { wholeScreen.bottom() - currentScreen.bottom() }; + strut.bottom_width = rect.height() + bottomOffset + (isMax ? 0 : DockSettings::self()->edgeMargins()); + strut.bottom_start = rect.x(); + strut.bottom_end = rect.x() + rect.width(); + break; + } + default: + break; + } + + KWindowSystem::setExtendedStrut(view->winId(), + strut.left_width, strut.left_start, strut.left_end, + strut.right_width, strut.right_start, strut.right_end, + strut.top_width, strut.top_start, strut.top_end, + strut.bottom_width, strut.bottom_start, strut.bottom_end + ); +} + +void XWindowInterface::startInitWindows() +{ + for (auto wid : KWindowSystem::self()->windows()) { + onWindowadded(wid); + } +} + +QString XWindowInterface::desktopFilePath(quint64 wid) +{ + const KWindowInfo info(wid, NET::Properties(), NET::WM2WindowClass | NET::WM2DesktopFileName); + return Utils::instance()->desktopPathFromMetadata(info.windowClassClass(), + NETWinInfo(QX11Info::connection(), wid, + QX11Info::appRootWindow(), + NET::WMPid, + NET::Properties2()).pid(), + info.windowClassName()); +} + +void XWindowInterface::setIconGeometry(quint64 wid, const QRect &rect) +{ + NETWinInfo info(QX11Info::connection(), + wid, + (WId) QX11Info::appRootWindow(), + NET::WMIconGeometry, + QFlags(1)); + NETRect nrect; + nrect.pos.x = rect.x(); + nrect.pos.y = rect.y(); + nrect.size.height = rect.height(); + nrect.size.width = rect.width(); + info.setIconGeometry(nrect); +} + +void XWindowInterface::onWindowadded(quint64 wid) +{ + if (isAcceptableWindow(wid)) { + emit windowAdded(wid); + } +} diff --git a/src/xwindowinterface.h b/src/xwindowinterface.h new file mode 100644 index 0000000..69157c9 --- /dev/null +++ b/src/xwindowinterface.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 ~ 2021 CyberOS Team. + * + * Author: rekols + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef XWINDOWINTERFACE_H +#define XWINDOWINTERFACE_H + +#include "applicationitem.h" +#include "docksettings.h" +#include + +// KLIB +#include +#include + +class XWindowInterface : public QObject +{ + Q_OBJECT + +public: + static XWindowInterface *instance(); + explicit XWindowInterface(QObject *parent = nullptr); + + void enableBlurBehind(QWindow *view, bool enable = true, const QRegion ®ion = QRegion()); + + WId activeWindow(); + void minimizeWindow(WId win); + void closeWindow(WId id); + void forceActiveWindow(WId win); + + QMap requestInfo(quint64 wid); + QString requestWindowClass(quint64 wid); + bool isAcceptableWindow(quint64 wid); + + void setViewStruts(QWindow *view, DockSettings::Direction direction, const QRect &rect, bool isMax = false); + + void startInitWindows(); + + QString desktopFilePath(quint64 wid); + + void setIconGeometry(quint64 wid, const QRect &rect); + +signals: + void windowAdded(quint64 wid); + void windowRemoved(quint64 wid); + void activeChanged(quint64 wid); + +private: + void onWindowadded(quint64 wid); +}; + +#endif // XWINDOWINTERFACE_H diff --git a/svg/dark/audio-volume-high-symbolic.svg b/svg/dark/audio-volume-high-symbolic.svg new file mode 100644 index 0000000..b151449 --- /dev/null +++ b/svg/dark/audio-volume-high-symbolic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/dark/audio-volume-low-symbolic.svg b/svg/dark/audio-volume-low-symbolic.svg new file mode 100644 index 0000000..935d1fe --- /dev/null +++ b/svg/dark/audio-volume-low-symbolic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/dark/audio-volume-medium-symbolic.svg b/svg/dark/audio-volume-medium-symbolic.svg new file mode 100644 index 0000000..06ff169 --- /dev/null +++ b/svg/dark/audio-volume-medium-symbolic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/dark/audio-volume-muted-symbolic.svg b/svg/dark/audio-volume-muted-symbolic.svg new file mode 100644 index 0000000..208e22e --- /dev/null +++ b/svg/dark/audio-volume-muted-symbolic.svg @@ -0,0 +1,17 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/svg/dark/battery-level-0-charging-symbolic.svg b/svg/dark/battery-level-0-charging-symbolic.svg new file mode 100644 index 0000000..4b9b263 --- /dev/null +++ b/svg/dark/battery-level-0-charging-symbolic.svg @@ -0,0 +1,68 @@ + + + + + + image/svg+xml + + + + + + + Paper Symbolic Icon Theme + + + + diff --git a/svg/dark/battery-level-0-symbolic.svg b/svg/dark/battery-level-0-symbolic.svg new file mode 100644 index 0000000..ba82892 --- /dev/null +++ b/svg/dark/battery-level-0-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + Paper Symbolic Icon Theme + + + diff --git a/svg/dark/battery-level-10-charging-symbolic.svg b/svg/dark/battery-level-10-charging-symbolic.svg new file mode 100644 index 0000000..1daaf02 --- /dev/null +++ b/svg/dark/battery-level-10-charging-symbolic.svg @@ -0,0 +1,9 @@ + + Paper Symbolic Icon Theme + + + + + + + diff --git a/svg/dark/battery-level-10-symbolic.svg b/svg/dark/battery-level-10-symbolic.svg new file mode 100644 index 0000000..82883bc --- /dev/null +++ b/svg/dark/battery-level-10-symbolic.svg @@ -0,0 +1,8 @@ + + Paper Symbolic Icon Theme + + + + + + diff --git a/svg/dark/battery-level-100-charging-symbolic.svg b/svg/dark/battery-level-100-charging-symbolic.svg new file mode 100644 index 0000000..d4ed5dd --- /dev/null +++ b/svg/dark/battery-level-100-charging-symbolic.svg @@ -0,0 +1,11 @@ + + Paper Symbolic Icon Theme + + + + + + + + + diff --git a/svg/dark/battery-level-100-symbolic.svg b/svg/dark/battery-level-100-symbolic.svg new file mode 100644 index 0000000..46c7a87 --- /dev/null +++ b/svg/dark/battery-level-100-symbolic.svg @@ -0,0 +1,8 @@ + + Paper Symbolic Icon Theme + + + + + + diff --git a/svg/dark/battery-level-20-charging-symbolic.svg b/svg/dark/battery-level-20-charging-symbolic.svg new file mode 100644 index 0000000..0a4453f --- /dev/null +++ b/svg/dark/battery-level-20-charging-symbolic.svg @@ -0,0 +1,80 @@ + + + + + + image/svg+xml + + + + + + + Paper Symbolic Icon Theme + + + + + + + diff --git a/svg/dark/battery-level-20-symbolic.svg b/svg/dark/battery-level-20-symbolic.svg new file mode 100644 index 0000000..2d1877f --- /dev/null +++ b/svg/dark/battery-level-20-symbolic.svg @@ -0,0 +1,71 @@ + + + + + + image/svg+xml + + + + + + + Paper Symbolic Icon Theme + + + + diff --git a/svg/dark/battery-level-30-charging-symbolic.svg b/svg/dark/battery-level-30-charging-symbolic.svg new file mode 100644 index 0000000..60ce649 --- /dev/null +++ b/svg/dark/battery-level-30-charging-symbolic.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/svg/dark/battery-level-30-symbolic.svg b/svg/dark/battery-level-30-symbolic.svg new file mode 100644 index 0000000..fdf956f --- /dev/null +++ b/svg/dark/battery-level-30-symbolic.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg/dark/battery-level-40-charging-symbolic.svg b/svg/dark/battery-level-40-charging-symbolic.svg new file mode 100644 index 0000000..60ce649 --- /dev/null +++ b/svg/dark/battery-level-40-charging-symbolic.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/svg/dark/battery-level-40-symbolic.svg b/svg/dark/battery-level-40-symbolic.svg new file mode 100644 index 0000000..122ac0f --- /dev/null +++ b/svg/dark/battery-level-40-symbolic.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg/dark/battery-level-50-charging-symbolic.svg b/svg/dark/battery-level-50-charging-symbolic.svg new file mode 100644 index 0000000..60ce649 --- /dev/null +++ b/svg/dark/battery-level-50-charging-symbolic.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/svg/dark/battery-level-50-symbolic.svg b/svg/dark/battery-level-50-symbolic.svg new file mode 100644 index 0000000..9cf18ef --- /dev/null +++ b/svg/dark/battery-level-50-symbolic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/svg/dark/battery-level-60-charging-symbolic.svg b/svg/dark/battery-level-60-charging-symbolic.svg new file mode 100644 index 0000000..238b45b --- /dev/null +++ b/svg/dark/battery-level-60-charging-symbolic.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/svg/dark/battery-level-60-symbolic.svg b/svg/dark/battery-level-60-symbolic.svg new file mode 100644 index 0000000..2ce5a18 --- /dev/null +++ b/svg/dark/battery-level-60-symbolic.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg/dark/battery-level-70-charging-symbolic.svg b/svg/dark/battery-level-70-charging-symbolic.svg new file mode 100644 index 0000000..238b45b --- /dev/null +++ b/svg/dark/battery-level-70-charging-symbolic.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/svg/dark/battery-level-70-symbolic.svg b/svg/dark/battery-level-70-symbolic.svg new file mode 100644 index 0000000..2ce5a18 --- /dev/null +++ b/svg/dark/battery-level-70-symbolic.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg/dark/battery-level-80-charging-symbolic.svg b/svg/dark/battery-level-80-charging-symbolic.svg new file mode 100644 index 0000000..e5facea --- /dev/null +++ b/svg/dark/battery-level-80-charging-symbolic.svg @@ -0,0 +1,11 @@ + + Paper Symbolic Icon Theme + + + + + + + + + diff --git a/svg/dark/battery-level-80-symbolic.svg b/svg/dark/battery-level-80-symbolic.svg new file mode 100644 index 0000000..778503b --- /dev/null +++ b/svg/dark/battery-level-80-symbolic.svg @@ -0,0 +1,8 @@ + + Paper Symbolic Icon Theme + + + + + + diff --git a/svg/dark/battery-level-90-charging-symbolic.svg b/svg/dark/battery-level-90-charging-symbolic.svg new file mode 100644 index 0000000..e5facea --- /dev/null +++ b/svg/dark/battery-level-90-charging-symbolic.svg @@ -0,0 +1,11 @@ + + Paper Symbolic Icon Theme + + + + + + + + + diff --git a/svg/dark/battery-level-90-symbolic.svg b/svg/dark/battery-level-90-symbolic.svg new file mode 100644 index 0000000..46be4f0 --- /dev/null +++ b/svg/dark/battery-level-90-symbolic.svg @@ -0,0 +1,72 @@ + + + + + + image/svg+xml + + + + + + + Paper Symbolic Icon Theme + + + + diff --git a/svg/dark/brightness.svg b/svg/dark/brightness.svg new file mode 100644 index 0000000..909fbdc --- /dev/null +++ b/svg/dark/brightness.svg @@ -0,0 +1,16 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg/dark/close_normal.svg b/svg/dark/close_normal.svg new file mode 100644 index 0000000..73e27b2 --- /dev/null +++ b/svg/dark/close_normal.svg @@ -0,0 +1,23 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/svg/dark/control.svg b/svg/dark/control.svg new file mode 100644 index 0000000..833f9e8 --- /dev/null +++ b/svg/dark/control.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/svg/dark/media-playback-pause-symbolic.svg b/svg/dark/media-playback-pause-symbolic.svg new file mode 100644 index 0000000..3e90132 --- /dev/null +++ b/svg/dark/media-playback-pause-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/dark/media-playback-start-symbolic.svg b/svg/dark/media-playback-start-symbolic.svg new file mode 100644 index 0000000..81730c4 --- /dev/null +++ b/svg/dark/media-playback-start-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/svg/dark/media-skip-backward-symbolic.svg b/svg/dark/media-skip-backward-symbolic.svg new file mode 100644 index 0000000..e35c9d5 --- /dev/null +++ b/svg/dark/media-skip-backward-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/dark/media-skip-forward-symbolic.svg b/svg/dark/media-skip-forward-symbolic.svg new file mode 100644 index 0000000..a2c94d7 --- /dev/null +++ b/svg/dark/media-skip-forward-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/dark/minimize_normal.svg b/svg/dark/minimize_normal.svg new file mode 100644 index 0000000..7adacee --- /dev/null +++ b/svg/dark/minimize_normal.svg @@ -0,0 +1,22 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/svg/dark/network-wired-activated.svg b/svg/dark/network-wired-activated.svg new file mode 100644 index 0000000..8fb6024 --- /dev/null +++ b/svg/dark/network-wired-activated.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/svg/dark/network-wired.svg b/svg/dark/network-wired.svg new file mode 100644 index 0000000..fe8e676 --- /dev/null +++ b/svg/dark/network-wired.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/svg/dark/network-wireless-connected-00.svg b/svg/dark/network-wireless-connected-00.svg new file mode 100644 index 0000000..77a2b43 --- /dev/null +++ b/svg/dark/network-wireless-connected-00.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/svg/dark/network-wireless-connected-100.svg b/svg/dark/network-wireless-connected-100.svg new file mode 100644 index 0000000..34f0a62 --- /dev/null +++ b/svg/dark/network-wireless-connected-100.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/svg/dark/network-wireless-connected-25.svg b/svg/dark/network-wireless-connected-25.svg new file mode 100644 index 0000000..ab60d64 --- /dev/null +++ b/svg/dark/network-wireless-connected-25.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/svg/dark/network-wireless-connected-50.svg b/svg/dark/network-wireless-connected-50.svg new file mode 100644 index 0000000..aae1995 --- /dev/null +++ b/svg/dark/network-wireless-connected-50.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/svg/dark/network-wireless-connected-75.svg b/svg/dark/network-wireless-connected-75.svg new file mode 100644 index 0000000..3c0d028 --- /dev/null +++ b/svg/dark/network-wireless-connected-75.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/svg/dark/restore_normal.svg b/svg/dark/restore_normal.svg new file mode 100644 index 0000000..57a8657 --- /dev/null +++ b/svg/dark/restore_normal.svg @@ -0,0 +1,26 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/svg/dark/settings.svg b/svg/dark/settings.svg new file mode 100644 index 0000000..9b3dff6 --- /dev/null +++ b/svg/dark/settings.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/svg/dark/system-shutdown-symbolic.svg b/svg/dark/system-shutdown-symbolic.svg new file mode 100644 index 0000000..99ba7f0 --- /dev/null +++ b/svg/dark/system-shutdown-symbolic.svg @@ -0,0 +1,60 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/svg/launcher.svg b/svg/launcher.svg new file mode 100755 index 0000000..c85b97a --- /dev/null +++ b/svg/launcher.svg @@ -0,0 +1,153 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/light/audio-volume-high-symbolic.svg b/svg/light/audio-volume-high-symbolic.svg new file mode 100644 index 0000000..0e998d7 --- /dev/null +++ b/svg/light/audio-volume-high-symbolic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/light/audio-volume-low-symbolic.svg b/svg/light/audio-volume-low-symbolic.svg new file mode 100644 index 0000000..ea3cb7d --- /dev/null +++ b/svg/light/audio-volume-low-symbolic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/light/audio-volume-medium-symbolic.svg b/svg/light/audio-volume-medium-symbolic.svg new file mode 100644 index 0000000..01ca6fa --- /dev/null +++ b/svg/light/audio-volume-medium-symbolic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/light/audio-volume-muted-symbolic.svg b/svg/light/audio-volume-muted-symbolic.svg new file mode 100644 index 0000000..173170f --- /dev/null +++ b/svg/light/audio-volume-muted-symbolic.svg @@ -0,0 +1,17 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/svg/light/battery-level-0-charging-symbolic.svg b/svg/light/battery-level-0-charging-symbolic.svg new file mode 100644 index 0000000..4b9b263 --- /dev/null +++ b/svg/light/battery-level-0-charging-symbolic.svg @@ -0,0 +1,68 @@ + + + + + + image/svg+xml + + + + + + + Paper Symbolic Icon Theme + + + + diff --git a/svg/light/battery-level-0-symbolic.svg b/svg/light/battery-level-0-symbolic.svg new file mode 100644 index 0000000..ba82892 --- /dev/null +++ b/svg/light/battery-level-0-symbolic.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + Paper Symbolic Icon Theme + + + diff --git a/svg/light/battery-level-10-charging-symbolic.svg b/svg/light/battery-level-10-charging-symbolic.svg new file mode 100644 index 0000000..972d4c1 --- /dev/null +++ b/svg/light/battery-level-10-charging-symbolic.svg @@ -0,0 +1,9 @@ + + Paper Symbolic Icon Theme + + + + + + + diff --git a/svg/light/battery-level-10-symbolic.svg b/svg/light/battery-level-10-symbolic.svg new file mode 100644 index 0000000..82883bc --- /dev/null +++ b/svg/light/battery-level-10-symbolic.svg @@ -0,0 +1,8 @@ + + Paper Symbolic Icon Theme + + + + + + diff --git a/svg/light/battery-level-100-charging-symbolic.svg b/svg/light/battery-level-100-charging-symbolic.svg new file mode 100644 index 0000000..52309c8 --- /dev/null +++ b/svg/light/battery-level-100-charging-symbolic.svg @@ -0,0 +1,11 @@ + + Paper Symbolic Icon Theme + + + + + + + + + diff --git a/svg/light/battery-level-100-symbolic.svg b/svg/light/battery-level-100-symbolic.svg new file mode 100644 index 0000000..681ffce --- /dev/null +++ b/svg/light/battery-level-100-symbolic.svg @@ -0,0 +1,8 @@ + + Paper Symbolic Icon Theme + + + + + + diff --git a/svg/light/battery-level-20-charging-symbolic.svg b/svg/light/battery-level-20-charging-symbolic.svg new file mode 100644 index 0000000..303898f --- /dev/null +++ b/svg/light/battery-level-20-charging-symbolic.svg @@ -0,0 +1,80 @@ + + + + + + image/svg+xml + + + + + + + Paper Symbolic Icon Theme + + + + + + + diff --git a/svg/light/battery-level-20-symbolic.svg b/svg/light/battery-level-20-symbolic.svg new file mode 100644 index 0000000..2d1877f --- /dev/null +++ b/svg/light/battery-level-20-symbolic.svg @@ -0,0 +1,71 @@ + + + + + + image/svg+xml + + + + + + + Paper Symbolic Icon Theme + + + + diff --git a/svg/light/battery-level-30-charging-symbolic.svg b/svg/light/battery-level-30-charging-symbolic.svg new file mode 100644 index 0000000..8f6dbef --- /dev/null +++ b/svg/light/battery-level-30-charging-symbolic.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/svg/light/battery-level-30-symbolic.svg b/svg/light/battery-level-30-symbolic.svg new file mode 100644 index 0000000..accd40c --- /dev/null +++ b/svg/light/battery-level-30-symbolic.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg/light/battery-level-40-charging-symbolic.svg b/svg/light/battery-level-40-charging-symbolic.svg new file mode 100644 index 0000000..8f6dbef --- /dev/null +++ b/svg/light/battery-level-40-charging-symbolic.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/svg/light/battery-level-40-symbolic.svg b/svg/light/battery-level-40-symbolic.svg new file mode 100644 index 0000000..b36b58e --- /dev/null +++ b/svg/light/battery-level-40-symbolic.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg/light/battery-level-50-charging-symbolic.svg b/svg/light/battery-level-50-charging-symbolic.svg new file mode 100644 index 0000000..8f6dbef --- /dev/null +++ b/svg/light/battery-level-50-charging-symbolic.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/svg/light/battery-level-50-symbolic.svg b/svg/light/battery-level-50-symbolic.svg new file mode 100644 index 0000000..0650da8 --- /dev/null +++ b/svg/light/battery-level-50-symbolic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/svg/light/battery-level-60-charging-symbolic.svg b/svg/light/battery-level-60-charging-symbolic.svg new file mode 100644 index 0000000..413e32e --- /dev/null +++ b/svg/light/battery-level-60-charging-symbolic.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/svg/light/battery-level-60-symbolic.svg b/svg/light/battery-level-60-symbolic.svg new file mode 100644 index 0000000..98f92d9 --- /dev/null +++ b/svg/light/battery-level-60-symbolic.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg/light/battery-level-70-charging-symbolic.svg b/svg/light/battery-level-70-charging-symbolic.svg new file mode 100644 index 0000000..413e32e --- /dev/null +++ b/svg/light/battery-level-70-charging-symbolic.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/svg/light/battery-level-70-symbolic.svg b/svg/light/battery-level-70-symbolic.svg new file mode 100644 index 0000000..98f92d9 --- /dev/null +++ b/svg/light/battery-level-70-symbolic.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg/light/battery-level-80-charging-symbolic.svg b/svg/light/battery-level-80-charging-symbolic.svg new file mode 100644 index 0000000..2700546 --- /dev/null +++ b/svg/light/battery-level-80-charging-symbolic.svg @@ -0,0 +1,11 @@ + + Paper Symbolic Icon Theme + + + + + + + + + diff --git a/svg/light/battery-level-80-symbolic.svg b/svg/light/battery-level-80-symbolic.svg new file mode 100644 index 0000000..4df07a7 --- /dev/null +++ b/svg/light/battery-level-80-symbolic.svg @@ -0,0 +1,8 @@ + + Paper Symbolic Icon Theme + + + + + + diff --git a/svg/light/battery-level-90-charging-symbolic.svg b/svg/light/battery-level-90-charging-symbolic.svg new file mode 100644 index 0000000..2700546 --- /dev/null +++ b/svg/light/battery-level-90-charging-symbolic.svg @@ -0,0 +1,11 @@ + + Paper Symbolic Icon Theme + + + + + + + + + diff --git a/svg/light/battery-level-90-symbolic.svg b/svg/light/battery-level-90-symbolic.svg new file mode 100644 index 0000000..6702735 --- /dev/null +++ b/svg/light/battery-level-90-symbolic.svg @@ -0,0 +1,72 @@ + + + + + + image/svg+xml + + + + + + + Paper Symbolic Icon Theme + + + + diff --git a/svg/light/bluetooth-symbolic.svg b/svg/light/bluetooth-symbolic.svg new file mode 100644 index 0000000..7e9605c --- /dev/null +++ b/svg/light/bluetooth-symbolic.svg @@ -0,0 +1,16 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg/light/brightness.svg b/svg/light/brightness.svg new file mode 100644 index 0000000..487abe9 --- /dev/null +++ b/svg/light/brightness.svg @@ -0,0 +1,16 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg/light/close_normal.svg b/svg/light/close_normal.svg new file mode 100644 index 0000000..82ae50e --- /dev/null +++ b/svg/light/close_normal.svg @@ -0,0 +1,23 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/svg/light/control.svg b/svg/light/control.svg new file mode 100644 index 0000000..d2d4a24 --- /dev/null +++ b/svg/light/control.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/svg/light/dark-mode.svg b/svg/light/dark-mode.svg new file mode 100644 index 0000000..79b4c10 --- /dev/null +++ b/svg/light/dark-mode.svg @@ -0,0 +1,58 @@ + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg/light/media-playback-pause-symbolic.svg b/svg/light/media-playback-pause-symbolic.svg new file mode 100644 index 0000000..4290326 --- /dev/null +++ b/svg/light/media-playback-pause-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/light/media-playback-start-symbolic.svg b/svg/light/media-playback-start-symbolic.svg new file mode 100644 index 0000000..8d57c19 --- /dev/null +++ b/svg/light/media-playback-start-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/svg/light/media-skip-backward-symbolic.svg b/svg/light/media-skip-backward-symbolic.svg new file mode 100644 index 0000000..1b85972 --- /dev/null +++ b/svg/light/media-skip-backward-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/light/media-skip-forward-symbolic.svg b/svg/light/media-skip-forward-symbolic.svg new file mode 100644 index 0000000..1193e44 --- /dev/null +++ b/svg/light/media-skip-forward-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/light/minimize_normal.svg b/svg/light/minimize_normal.svg new file mode 100644 index 0000000..c5562db --- /dev/null +++ b/svg/light/minimize_normal.svg @@ -0,0 +1,22 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/svg/light/network-wired-activated.svg b/svg/light/network-wired-activated.svg new file mode 100644 index 0000000..62a3edc --- /dev/null +++ b/svg/light/network-wired-activated.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/light/network-wired.svg b/svg/light/network-wired.svg new file mode 100644 index 0000000..22b2547 --- /dev/null +++ b/svg/light/network-wired.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/svg/light/network-wireless-connected-00.svg b/svg/light/network-wireless-connected-00.svg new file mode 100644 index 0000000..b2cc080 --- /dev/null +++ b/svg/light/network-wireless-connected-00.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/svg/light/network-wireless-connected-100.svg b/svg/light/network-wireless-connected-100.svg new file mode 100644 index 0000000..438ec59 --- /dev/null +++ b/svg/light/network-wireless-connected-100.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/svg/light/network-wireless-connected-25.svg b/svg/light/network-wireless-connected-25.svg new file mode 100644 index 0000000..eae90db --- /dev/null +++ b/svg/light/network-wireless-connected-25.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/svg/light/network-wireless-connected-50.svg b/svg/light/network-wireless-connected-50.svg new file mode 100644 index 0000000..63e1de0 --- /dev/null +++ b/svg/light/network-wireless-connected-50.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/svg/light/network-wireless-connected-75.svg b/svg/light/network-wireless-connected-75.svg new file mode 100644 index 0000000..7d28375 --- /dev/null +++ b/svg/light/network-wireless-connected-75.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/svg/light/restore_normal.svg b/svg/light/restore_normal.svg new file mode 100644 index 0000000..c6680a0 --- /dev/null +++ b/svg/light/restore_normal.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/svg/light/settings.svg b/svg/light/settings.svg new file mode 100644 index 0000000..5481d44 --- /dev/null +++ b/svg/light/settings.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/svg/light/system-shutdown-symbolic.svg b/svg/light/system-shutdown-symbolic.svg new file mode 100644 index 0000000..dd97cbf --- /dev/null +++ b/svg/light/system-shutdown-symbolic.svg @@ -0,0 +1,64 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/svg/media-playback-pause-symbolic.svg b/svg/media-playback-pause-symbolic.svg new file mode 100644 index 0000000..4290326 --- /dev/null +++ b/svg/media-playback-pause-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/media-playback-start-symbolic.svg b/svg/media-playback-start-symbolic.svg new file mode 100644 index 0000000..8d57c19 --- /dev/null +++ b/svg/media-playback-start-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/svg/media-skip-backward-symbolic.svg b/svg/media-skip-backward-symbolic.svg new file mode 100644 index 0000000..1b85972 --- /dev/null +++ b/svg/media-skip-backward-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/media-skip-forward-symbolic.svg b/svg/media-skip-forward-symbolic.svg new file mode 100644 index 0000000..1193e44 --- /dev/null +++ b/svg/media-skip-forward-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/translations/en_US.ts b/translations/en_US.ts new file mode 100644 index 0000000..c284102 --- /dev/null +++ b/translations/en_US.ts @@ -0,0 +1,58 @@ + + + + + AppItem + + + Open + + + + + Unpin + + + + + Pin + + + + + Close All + + + + + ControlCenter + + + Wi-Fi + + + + + + On + + + + + + + Off + + + + + Bluetooth + + + + + Dark Mode + + + + diff --git a/translations/zh_CN.ts b/translations/zh_CN.ts new file mode 100644 index 0000000..7abec32 --- /dev/null +++ b/translations/zh_CN.ts @@ -0,0 +1,65 @@ + + + + + AppItem + + + Open + 打开 + + + + Unpin + 取消固定 + + + + Pin + 固定 + + + + Close All + 关闭所有 + + + + ApplicationModel + + Launcher + 应用启动器 + + + + ControlCenter + + + Wi-Fi + 无线网络 + + + + + On + 打开 + + + + + + Off + 关闭 + + + + Bluetooth + 蓝牙 + + + + Dark Mode + 深色模式 + + +