commit 6d82581776e6a5ba6696cc698aeeb55a18f38c19 Author: cutefishd Date: Tue Mar 16 15:02:20 2021 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f50db9 --- /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..0c40dd3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.14) + +project(cutefish-filemanager LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt5 COMPONENTS Core Quick Concurrent DBus LinguistTools REQUIRED) +find_package(KF5KIO) +find_package(MeuiKit REQUIRED) + +add_executable(cutefish-filemanager + src/main.cpp + src/fm.cpp + src/fmh.cpp + src/fmstatic.cpp + src/fmlist.cpp + src/handy.cpp + src/placeslist.cpp + src/pathlist.cpp + src/baselist.cpp + src/basemodel.cpp + src/rubberband.cpp + src/iconthemeprovider.cpp + + src/lib/foldermodel.cpp + src/lib/positioner.cpp + src/lib/itemviewadapter.cpp + src/lib/fileitemactions.cpp + src/lib/placesmodel.cpp + src/lib/placesitem.cpp + + src/dialogs/propertiesdialog.cpp + + src/desktop/desktopview.cpp + src/desktop/desktopsettings.cpp + + qml.qrc +) + +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) + +target_link_libraries(cutefish-filemanager + PRIVATE + Qt5::Core + Qt5::GuiPrivate + Qt5::Quick + Qt5::Concurrent + Qt5::DBus + + KF5::KIOCore + KF5::KIOFileWidgets + KF5::KIOWidgets + + MeuiKit +) + +install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION /usr/bin) +install(FILES cutefish-filemanager.desktop DESTINATION "/usr/share/applications") +install(FILES ${QM_FILES} DESTINATION /usr/share/cutefish-filemanager/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..e967892 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# File Manager + +Cutefish File Manager + +## Dependencies + +```shell +sudo pacman -S extra-cmake-modules qt5-base qt5-quickcontrols2 taglib kio +``` + +## Build + +```shell +mkdir build +cd build +cmake .. +make +``` + +## Install + +```shell +sudo make install +``` + +## License + +This project has been licensed by GPLv3. \ No newline at end of file diff --git a/cutefish-filemanager.desktop b/cutefish-filemanager.desktop new file mode 100644 index 0000000..457fc85 --- /dev/null +++ b/cutefish-filemanager.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Name=File Manager +Name[zh_CN]=文件管理器 +GenericName=File Manager +Comment=Cutefish File Manager +Exec=cutefish-fm %U +MimeType=inode/directory; +Icon=file-system-manager +Categories=FileManager;Utility;Core;Qt; +StartupNotify=true diff --git a/images/dark/go-next.svg b/images/dark/go-next.svg new file mode 100644 index 0000000..c93229d --- /dev/null +++ b/images/dark/go-next.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/images/dark/go-previous.svg b/images/dark/go-previous.svg new file mode 100644 index 0000000..73d599f --- /dev/null +++ b/images/dark/go-previous.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/images/dark/grid.svg b/images/dark/grid.svg new file mode 100644 index 0000000..16cf854 --- /dev/null +++ b/images/dark/grid.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/images/dark/list.svg b/images/dark/list.svg new file mode 100644 index 0000000..5517525 --- /dev/null +++ b/images/dark/list.svg @@ -0,0 +1,105 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/images/folder-desktop.svg b/images/folder-desktop.svg new file mode 100755 index 0000000..e1b94f9 --- /dev/null +++ b/images/folder-desktop.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/images/folder-document.svg b/images/folder-document.svg new file mode 100755 index 0000000..3e8c5af --- /dev/null +++ b/images/folder-document.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/images/folder-download.svg b/images/folder-download.svg new file mode 100755 index 0000000..efcf549 --- /dev/null +++ b/images/folder-download.svg @@ -0,0 +1,59 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/images/folder-home.svg b/images/folder-home.svg new file mode 100755 index 0000000..c2d1900 --- /dev/null +++ b/images/folder-home.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/images/folder-music.svg b/images/folder-music.svg new file mode 100755 index 0000000..8b15f75 --- /dev/null +++ b/images/folder-music.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/images/folder-picture.svg b/images/folder-picture.svg new file mode 100755 index 0000000..dd86d5d --- /dev/null +++ b/images/folder-picture.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/images/folder-video.svg b/images/folder-video.svg new file mode 100755 index 0000000..a1e0c3e --- /dev/null +++ b/images/folder-video.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/images/light/go-next.svg b/images/light/go-next.svg new file mode 100644 index 0000000..2420682 --- /dev/null +++ b/images/light/go-next.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/images/light/go-previous.svg b/images/light/go-previous.svg new file mode 100644 index 0000000..197b0ac --- /dev/null +++ b/images/light/go-previous.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/images/light/grid.svg b/images/light/grid.svg new file mode 100644 index 0000000..e035d6c --- /dev/null +++ b/images/light/grid.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/images/light/list.svg b/images/light/list.svg new file mode 100644 index 0000000..603a6d1 --- /dev/null +++ b/images/light/list.svg @@ -0,0 +1,105 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/images/user-trash.svg b/images/user-trash.svg new file mode 100755 index 0000000..4f37501 --- /dev/null +++ b/images/user-trash.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/qml.qrc b/qml.qrc new file mode 100644 index 0000000..4b7e4ee --- /dev/null +++ b/qml.qrc @@ -0,0 +1,40 @@ + + + qml/main.qml + qml/SideBar.qml + qml/SidebarItem.qml + qml/FolderListView.qml + qml/PathBar.qml + qml/FolderListDelegate.qml + images/dark/go-next.svg + images/dark/go-previous.svg + images/dark/grid.svg + images/dark/list.svg + images/light/go-next.svg + images/light/go-previous.svg + images/light/grid.svg + images/light/list.svg + qml/IconButton.qml + images/folder-download.svg + images/folder-home.svg + images/folder-music.svg + images/folder-picture.svg + images/folder-video.svg + images/folder-document.svg + qml/GlobalSettings.qml + qml/FolderIconView.qml + qml/FolderIconDelegate.qml + qml/BrowserView.qml + qml/ItemMenu.qml + qml/BrowserMenu.qml + qml/Desktop/DesktopFolderView.qml + qml/Desktop/FolderViewDropArea.qml + qml/Desktop/FolderItemDelegate.qml + qml/Desktop/Desktop.qml + qml/Desktop/FolderTools.js + qml/Dialogs/PropertiesDialog.qml + qml/IconDelegate.qml + images/folder-desktop.svg + images/user-trash.svg + + diff --git a/qml/BrowserMenu.qml b/qml/BrowserMenu.qml new file mode 100644 index 0000000..e341624 --- /dev/null +++ b/qml/BrowserMenu.qml @@ -0,0 +1,61 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import Cutefish.FileManager 1.0 +import MeuiKit 1.0 as Meui + +Menu { + id: control + + property FMList currentList + + signal emptyTrashClicked() + signal propertiesClicked() + signal selectAllClicked() + + MenuItem { + id: newFolderItem + text: qsTr("New Folder") + enabled: currentList.pathType !== FMList.TRASH_PATH + } + + MenuSeparator { + visible: newFolderItem.visible && pasteItem.visible + } + + MenuItem { + id: pasteItem + text: qsTr("Paste") + onTriggered: paste() + enabled: currentList.pathType !== FMList.TRASH_PATH + } + + MenuItem { + text: qsTr("Select All") + onTriggered: control.selectAllClicked() + } + + MenuItem { + id: terminal + text: qsTr("Open in Terminal") + } + + MenuItem { + id: properties + text: qsTr("Properties") + onTriggered: { + propertiesClicked() + close() + } + } + + MenuItem { + id: emptyItem + text: qsTr("Empty Trash") + visible: currentList.pathType === FMList.TRASH_PATH + onTriggered: control.emptyTrashClicked() + } + + function show(parent = control, x, y) { + popup(parent, x, y) + } +} diff --git a/qml/BrowserView.qml b/qml/BrowserView.qml new file mode 100644 index 0000000..6b71668 --- /dev/null +++ b/qml/BrowserView.qml @@ -0,0 +1,107 @@ +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 Cutefish.FileManager 1.0 as FM + +Item { + id: control + + property alias model: dirModel + property alias url: dirModel.url + property alias currentView: viewLoader.item + + signal openPathBar + + FM.FolderModel { + id: dirModel + sortDirsFirst: true + parseDesktopFiles: true + url: dirModel.homePath() + previews: true + previewPlugins: [] + } + + FM.Positioner { + id: positioner + folderModel: dirModel + enabled: true + } + + Rectangle { + anchors.fill: parent + anchors.topMargin: 0 + anchors.leftMargin: Meui.Theme.smallRadius / 2 + anchors.rightMargin: Meui.Theme.smallRadius + anchors.bottomMargin: Meui.Theme.smallRadius + radius: Meui.Theme.smallRadius + color: Meui.Theme.backgroundColor + + Label { + anchors.centerIn: parent + text: qsTr("No Files") + font.pointSize: 20 + visible: dirModel.status === FM.FolderModel.Ready && currentView.count === 0 + } + } + + Loader { + id: viewLoader + anchors.fill: parent + anchors.bottomMargin: Meui.Units.largeSpacing + sourceComponent: switch (settings.viewMethod) { + case 0: return listViewBrowser + case 1: return gridViewBrowser + } + } + + Component { + id: listViewBrowser + + FolderListView { + id: _listViewBrowser + anchors.fill: parent + model: dirModel + + leftMargin: Meui.Units.largeSpacing + Meui.Units.smallSpacing + rightMargin: Meui.Units.largeSpacing + Meui.Units.smallSpacing + topMargin: Meui.Units.smallSpacing + bottomMargin: Meui.Units.largeSpacing * 2 + + delegate: FolderListDelegate { + id: listDelegate + width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin + height: 48 + } + } + } + + Component { + id: gridViewBrowser + + FolderIconView { + id: _gridViewBrowser + anchors.fill: parent + model: dirModel + + leftMargin: Meui.Units.largeSpacing + rightMargin: Meui.Units.largeSpacing + + delegate: FolderIconDelegate { + id: iconDelegate + height: _gridViewBrowser.cellHeight + width: _gridViewBrowser.cellWidth + } + } + } + + Component.onCompleted: { + control.currentView.forceActiveFocus() + } + + function openFolder(url) { + dirModel.url = url + } +} diff --git a/qml/Desktop/Desktop.qml b/qml/Desktop/Desktop.qml new file mode 100644 index 0000000..ff29d65 --- /dev/null +++ b/qml/Desktop/Desktop.qml @@ -0,0 +1,97 @@ +import QtQuick 2.4 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.0 + +import Qt.labs.platform 1.0 +import Cutefish.FileManager 1.0 +import MeuiKit 1.0 as Meui + +FolderViewDropArea { + id: root + visible: true + + preventStealing: true + + property bool containsDrag: false + + folderView: folderViewLayer.item + + function isDrag(fromX, fromY, toX, toY) { + var length = Math.abs(fromX - toX) + Math.abs(fromY - toY); + return length >= Qt.styleHints.startDragDistance; + } + + function isFileDrag(event) { + var taskUrl = event.mimeData.formats.indexOf("text/x-orgkdeplasmataskmanager_taskurl") !== -1; + var arkService = event.mimeData.formats.indexOf("application/x-kde-ark-dndextract-service") !== -1; + var arkPath = event.mimeData.formats.indexOf("application/x-kde-ark-dndextract-path") !== -1; + return (event.mimeData.hasUrls || taskUrl || (arkService && arkPath)); + } + + onDragEnter: { + if (!isFileDrag(event)) + event.ignore(); + + // Firefox tabs are regular drags. Since all of our drop handling is asynchronous + // we would accept this drop and have Firefox not spawn a new window. (Bug 337711) + if (event.mimeData.formats.indexOf("application/x-moz-tabbrowser-tab") > -1) { + event.ignore(); + } + } + + onDragMove: { + + } + + onDragLeave: { + + } + + onDrop: { + + } + + DesktopSettings { + id: settings + } + + Image { + id: wallpaper + anchors.fill: parent + source: "file://" + settings.wallpaper + sourceSize: Qt.size(width, height) + fillMode: Image.PreserveAspectCrop + clip: true + cache: false + + ColorOverlay { + id: dimsWallpaper + anchors.fill: wallpaper + source: wallpaper + color: "#000000" + opacity: Meui.Theme.darkMode && settings.dimsWallpaper ? 0.4 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + + } + } + + Loader { + id: folderViewLayer + anchors.fill: parent + + property bool ready: status == Loader.Ready + property Item view: item ? item : null + property QtObject model: item ? item.model : null + + focus: true + active: true + asynchronous: false + source: "DesktopFolderView.qml" + } +} diff --git a/qml/Desktop/DesktopFolderView.qml b/qml/Desktop/DesktopFolderView.qml new file mode 100644 index 0000000..06398d5 --- /dev/null +++ b/qml/Desktop/DesktopFolderView.qml @@ -0,0 +1,1053 @@ +import QtQuick 2.12 +import QtQml 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import Cutefish.FileManager 1.0 as FM +import MeuiKit 1.0 as Meui + +import "FolderTools.js" as FolderTools + +FocusScope { + id: control + + property QtObject model: dir + property Item rubberBand: null + + property alias positions: positioner.positions + property alias scrollLeft: gridView.scrollLeft + property alias scrollRight: gridView.scrollRight + property alias scrollUp: gridView.scrollUp + property alias scrollDown: gridView.scrollDown + property alias hoveredItem: gridView.hoveredItem + property int previouslySelectedItemIndex: -1 + + property alias cellWidth: gridView.cellWidth + property alias cellHeight: gridView.cellHeight + + function positionViewAtBeginning() { + gridView.positionViewAtBeginning(); + } + + function rename() { + if (gridView.currentIndex != -1) { + var renameAction = folderView.model.action("rename"); + if (renameAction && !renameAction.enabled) { + return; + } + + if (!editor) { + editor = editorComponent.createObject(listener); + } + + editor.targetItem = gridView.currentItem; + } + } + + function cancelRename() { + if (editor) { + editor.targetItem = null; + } + } + + function handleDragMove(x, y) { + var child = childAt(x, y); + + if (child !== null) { + hoveredItem = null; + } else { + var pos = mapToItem(gridView.contentItem, x, y); + var item = gridView.itemAt(pos.x, pos.y); + + if (item && item.isDir) { + hoveredItem = item; + } else { + hoveredItem = null; + } + } + } + + function endDragMove() { + if (hoveredItem) { + hoveredItem = null; + } + } + + function dropItemAt(pos) { + var item = gridView.itemAt(pos.x, pos.y); + + if (item) { + if (item.blank) { + return -1; + } + + var hOffset = Math.abs(Math.min(gridView.contentX, gridView.originX)); + var hPos = mapToItem(item.frame, pos.x + hOffset, pos.y); + + if ((hPos.x < 0 || hPos.y < 0 || hPos.x > item.frame.width || hPos.y > item.frame.height)) { + return -1; + } else { + return positioner.map(item.index); + } + } + + return -1; + } + + function drop(target, event, pos) { + var dropPos = mapToItem(gridView.contentItem, pos.x, pos.y); + var dropIndex = gridView.indexAt(dropPos.x, dropPos.y); + var dragPos = mapToItem(gridView.contentItem, listener.dragX, listener.dragY); + var dragIndex = gridView.indexAt(dragPos.x, dragPos.y); + + if (listener.dragX === -1 || dragIndex !== dropIndex) { + dir.drop(target, event, dropItemAt(dropPos), root.isContainment && !plasmoid.immutable); + } + } + + MouseArea { + id: listener + anchors.fill: parent + z: 999 + + property alias hoveredItem: gridView.hoveredItem + + property Item pressedItem: null + property int pressX: -1 + property int pressY: -1 + property int dragX: -1 + property int dragY: -1 + property variant cPress: null + property bool doubleClickInProgress: false + + Keys.forwardTo: gridView + + hoverEnabled: true + + onPressXChanged: { + cPress = mapToItem(gridView.contentItem, pressX, pressY); + } + + onPressYChanged: { + cPress = mapToItem(gridView.contentItem, pressX, pressY); + } + + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: { + gridView.focus = true + gridView.forceActiveFocus() + + if (mouse.source === Qt.MouseEventSynthesizedByQt) { + var index = gridView.indexAt(mouse.x, mouse.y); + var indexItem = gridView.itemAtIndex(index); + if (indexItem && indexItem.iconArea) { + gridView.currentIndex = index; + hoveredItem = indexItem; + } else { + hoveredItem = null; + } + } + + pressX = mouse.x; + pressY = mouse.y; + + if (!hoveredItem || hoveredItem.blank) { + if (!gridView.ctrlPressed) { + gridView.currentIndex = -1; + previouslySelectedItemIndex = -1; + dir.clearSelection(); + } + + if (mouse.buttons & Qt.RightButton) { + clearPressState(); + dir.openContextMenu(null, mouse.modifiers); + mouse.accepted = true; + } + } else { + pressedItem = hoveredItem; + + if (gridView.shiftPressed && gridView.currentIndex != -1) { + positioner.setRangeSelected(gridView.anchorIndex, hoveredItem.index); + } else { + if (!gridView.ctrlPressed && !dir.isSelected(positioner.map(hoveredItem.index))) { + previouslySelectedItemIndex = -1; + dir.clearSelection(); + } + + if (gridView.ctrlPressed) { + dir.toggleSelected(positioner.map(hoveredItem.index)); + } else { + dir.setSelected(positioner.map(hoveredItem.index)); + } + + gridView.currentIndex = hoveredItem.index; + + if (mouse.buttons & Qt.RightButton) { + clearPressState(); + + dir.openContextMenu(null, mouse.modifiers); + mouse.accepted = true; + } + } + } + + // control.pressed(); + } + + onPositionChanged: { + gridView.ctrlPressed = (mouse.modifiers & Qt.ControlModifier); + gridView.shiftPressed = (mouse.modifiers & Qt.ShiftModifier); + + var cPos = mapToItem(gridView.contentItem, mouse.x, mouse.y); + var item = gridView.itemAt(cPos.x, cPos.y); + var leftEdge = Math.min(gridView.contentX, gridView.originX); + + if (!item || item.blank) { + if (gridView.hoveredItem && !root.containsDrag) { + gridView.hoveredItem = null; + } + } else { + var fPos = mapToItem(item.frame, mouse.x, mouse.y); + + if (fPos.x < 0 || fPos.y < 0 || fPos.x > item.frame.width || fPos.y > item.frame.height) { + gridView.hoveredItem = null; + } else { + gridView.hoveredItem = item + } + } + + // Trigger autoscroll. + if (pressX != -1) { + gridView.scrollLeft = (mouse.x <= 0 && gridView.contentX > leftEdge); + gridView.scrollRight = (mouse.x >= gridView.width + && gridView.contentX < gridView.contentItem.width - gridView.width); + gridView.scrollUp = (mouse.y <= 0 && gridView.contentY > 0); + gridView.scrollDown = (mouse.y >= gridView.height + && gridView.contentY < gridView.contentItem.height - gridView.height); + } + + // Update rubberband geometry. + if (control.rubberBand) { + var rB = control.rubberBand; + + if (cPos.x < cPress.x) { + rB.x = Math.max(leftEdge, cPos.x); + rB.width = Math.abs(rB.x - cPress.x); + } else { + rB.x = cPress.x; + var ceil = Math.max(gridView.width, gridView.contentItem.width) + leftEdge; + rB.width = Math.min(ceil - rB.x, Math.abs(rB.x - cPos.x)); + } + + if (cPos.y < cPress.y) { + rB.y = Math.max(0, cPos.y); + rB.height = Math.abs(rB.y - cPress.y); + } else { + rB.y = cPress.y; + var ceil = Math.max(gridView.height, gridView.contentItem.height); + rB.height = Math.min(ceil - rB.y, Math.abs(rB.y - cPos.y)); + } + + // Ensure rubberband is at least 1px in size or else it will become + // invisible and not match any items. + rB.width = Math.max(1, rB.width); + rB.height = Math.max(1, rB.height); + + gridView.rectangleSelect(rB.x, rB.y, rB.width, rB.height); + + return; + } + + // Drag initiation. + if (pressX != -1 && root.isDrag(pressX, pressY, mouse.x, mouse.y)) { + if (pressedItem != null && dir.isSelected(positioner.map(pressedItem.index))) { + dragX = mouse.x; + dragY = mouse.y; + gridView.verticalDropHitscanOffset = pressedItem.iconArea.y + (pressedItem.iconArea.height / 2) + dir.dragSelected(mouse.x, mouse.y); + dragX = -1; + dragY = -1; + clearPressState(); + } else { + dir.pinSelection(); + control.rubberBand = rubberBandObject.createObject(gridView.contentItem, {x: cPress.x, y: cPress.y}) + gridView.interactive = false; + } + } + } + + onCanceled: pressCanceled() + onReleased: pressCanceled() + + onDoubleClicked: { + clearPressState() + + if (!hoveredItem || hoveredItem.blank || gridView.currentIndex == -1 || gridView.ctrlPressed || gridView.shiftPressed) { + return; + } + + if (mouse.button === Qt.LeftButton) + dir.runSelected() + } + + onClicked: { + clearPressState(); + + if (!hoveredItem || hoveredItem.blank || gridView.currentIndex == -1 || gridView.ctrlPressed || gridView.shiftPressed) { + return; + } + + var pos = mapToItem(hoveredItem, mouse.x, mouse.y); + + // Moving from an item to its preview popup dialog doesn't unset hoveredItem + // even though the cursor has left it, so we need to check whether the click + // actually occurred inside the item we expect it in before going ahead. If it + // didn't, clean up (e.g. dismissing the dialog as a side-effect of unsetting + // hoveredItem) and abort. + if (pos.x < 0 || pos.x > hoveredItem.width || pos.y < 0 || pos.y > hoveredItem.height) { + hoveredItem = null; + previouslySelectedItemIndex = -1; + dir.clearSelection(); + + return; + // If the hoveredItem is clicked while having a preview popup dialog open, + // only dismiss the dialog and abort. + } + } + + onContainsMouseChanged: { + if (!containsMouse && !control.rubberBand) { + clearPressState(); + + if (gridView.hoveredItem) { + gridView.hoveredItem = null; + } + } + } + + function pressCanceled() { + if (control.rubberBand) { + control.rubberBand.close() + control.rubberBand = null + + gridView.interactive = true; + gridView.cachedRectangleSelection = null; + dir.unpinSelection(); + } + + clearPressState(); + gridView.cancelAutoscroll(); + } + + function clearPressState() { + pressedItem = null; + pressX = -1; + pressY = -1; + } + } + + GridView { + id: gridView + anchors.fill: parent + + property int verticalDropHitscanOffset: 0 + + property Item hoveredItem: null + + property int anchorIndex: 0 + property bool ctrlPressed: false + property bool shiftPressed: false + + property bool overflowing: (visibleArea.heightRatio < 1.0 || visibleArea.widthRatio < 1.0) + + property bool scrollLeft: false + property bool scrollRight: false + property bool scrollUp: false + property bool scrollDown: false + + property variant cachedRectangleSelection: null + + flow: GridView.FlowTopToBottom + + currentIndex: -1 + + keyNavigationWraps: false + boundsBehavior: Flickable.StopAtBounds + + focus: true + + onActiveFocusChanged: { + if (!activeFocus) { + dir.clearSelection() + } + } + + property var iconSize: 150 + Meui.Units.largeSpacing + + cellWidth: { + var extraWidth = calcExtraSpacing(iconSize, gridView.width - leftMargin - rightMargin); + return iconSize + extraWidth; + } + + cellHeight: { + var extraHeight = calcExtraSpacing(iconSize, gridView.height - topMargin - bottomMargin); + return iconSize + extraHeight; + } + + function calcExtraSpacing(cellSize, containerSize) { + var availableColumns = Math.floor(containerSize / cellSize); + var extraSpacing = 0; + if (availableColumns > 0) { + var allColumnSize = availableColumns * cellSize; + var extraSpace = Math.max(containerSize - allColumnSize, 0); + extraSpacing = extraSpace / availableColumns; + } + return Math.floor(extraSpacing); + } + + function updateSelection(modifier) { + if (modifier & Qt.ShiftModifier) { + positioner.setRangeSelected(anchorIndex, currentIndex); + } else { + dir.clearSelection(); + dir.setSelected(positioner.map(currentIndex)); + if (currentIndex == -1) { + previouslySelectedItemIndex = -1; + } + previouslySelectedItemIndex = currentIndex; + } + } + + leftMargin: desktopView.screenAvailableRect ? desktopView.screenAvailableRect.x : 0 + topMargin: desktopView.screenAvailableRect ? desktopView.screenAvailableRect.y : 0 + rightMargin: desktopView.screenRect.width - (desktopView.screenAvailableRect.x + desktopView.screenAvailableRect.width) + bottomMargin: desktopView.screenRect.height - (desktopView.screenAvailableRect.y + desktopView.screenAvailableRect.height) + + delegate: FolderItemDelegate { + width: gridView.cellWidth + height: gridView.cellHeight + } + + onContentXChanged: { + // cancelRename() + + dir.setDragHotSpotScrollOffset(contentX, contentY); + + if (contentX == 0) { + scrollLeft = false; + } + + if (contentX == contentItem.width - width) { + scrollRight = false; + } + + // Update rubberband geometry. + if (control.rubberBand) { + var rB = control.rubberBand; + + if (scrollLeft) { + rB.x = Math.min(gridView.contentX, gridView.originX); + rB.width = listener.cPress.x; + } + + if (scrollRight) { + var lastCol = gridView.contentX + gridView.width; + rB.width = lastCol - rB.x; + } + + gridView.rectangleSelect(rB.x, rB.y, rB.width, rB.height); + } + } + + onContentYChanged: { + // cancelRename(); + + dir.setDragHotSpotScrollOffset(contentX, contentY); + + if (contentY == 0) { + scrollUp = false; + } + + if (contentY == contentItem.height - height) { + scrollDown = false; + } + + // Update rubberband geometry. + if (control.rubberBand) { + var rB = control.rubberBand; + + if (scrollUp) { + rB.y = 0; + rB.height = listener.cPress.y; + } + + if (scrollDown) { + var lastRow = gridView.contentY + gridView.height; + rB.height = lastRow - rB.y; + } + + gridView.rectangleSelect(rB.x, rB.y, rB.width, rB.height); + } + } + + onFlowChanged: { + // FIXME TODO: Preserve positions. + if (positioner.enabled) { + positioner.reset(); + } + } + + onLayoutDirectionChanged: { + // FIXME TODO: Preserve positions. + if (positioner.enabled) { + positioner.reset(); + } + } + + onCurrentIndexChanged: { + positionViewAtIndex(currentIndex, GridView.Contain); + } + + onCachedRectangleSelectionChanged: { + if (cachedRectangleSelection == null) { + return; + } + + if (cachedRectangleSelection.length) { + // Set current index to start of selection. + // cachedRectangleSelection is pre-sorted. + currentIndex = cachedRectangleSelection[0]; + } + + dir.updateSelection(cachedRectangleSelection.map(positioner.map), gridView.ctrlPressed); + } + + Behavior on contentX { id: smoothX; enabled: false; SmoothedAnimation { velocity: 700 } } + Behavior on contentY { id: smoothY; enabled: false; SmoothedAnimation { velocity: 700 } } + + Keys.onPressed: { + event.accepted = true + + if (event.matches(StandardKey.Delete)) { + if (dir.hasSelection()) { + dir.action("trash").trigger(); + } + } else if (event.key === Qt.Key_Control) { + ctrlPressed = true; + } else if (event.key === Qt.Key_Shift) { + shiftPressed = true; + + if (currentIndex != -1) { + anchorIndex = currentIndex; + } + } else if (event.key === Qt.Key_Home) { + currentIndex = 0; + updateSelection(event.modifiers); + } else if (event.key === Qt.Key_End) { + currentIndex = count - 1; + updateSelection(event.modifiers); + } else if (event.matches(StandardKey.Copy)) { + dir.copy(); + } else if (event.matches(StandardKey.Paste)) { + dir.paste(); + } else if (event.matches(StandardKey.Cut)) { + dir.cut(); + } else if (event.matches(StandardKey.Undo)) { + dir.undo(); + } else if (event.matches(StandardKey.Refresh)) { + dir.refresh(); + } else if (event.matches(StandardKey.SelectAll)) { + positioner.setRangeSelected(0, count - 1); + } else { + event.accepted = false; + } + } + + Keys.onReturnPressed: { + if (event.modifiers === Qt.AltModifier) { + dir.openPropertiesDialog(); + } else { + dir.runSelected() + } + } + + Keys.onEnterPressed: Keys.returnPressed(event) + + Keys.onReleased: { + if (event.key === Qt.Key_Control) { + ctrlPressed = false; + } else if (event.key === Qt.Key_Shift) { + shiftPressed = false; + anchorIndex = 0; + } + } + + Keys.onLeftPressed: { + if (positioner.enabled) { + var newIndex = positioner.nearestItem(currentIndex, + FolderTools.effectiveNavDirection(gridView.flow, gridView.effectiveLayoutDirection, Qt.LeftArrow)); + + if (newIndex !== -1) { + currentIndex = newIndex; + updateSelection(event.modifiers); + } + } else { + var oldIndex = currentIndex; + + moveCurrentIndexLeft(); + + if (oldIndex === currentIndex) { + return; + } + + updateSelection(event.modifiers); + } + } + + Keys.onRightPressed: { + if (positioner.enabled) { + var newIndex = positioner.nearestItem(currentIndex, + FolderTools.effectiveNavDirection(gridView.flow, gridView.effectiveLayoutDirection, Qt.RightArrow)); + + if (newIndex !== -1) { + currentIndex = newIndex; + updateSelection(event.modifiers); + } + } else { + var oldIndex = currentIndex; + + moveCurrentIndexRight(); + + if (oldIndex === currentIndex) { + return; + } + + updateSelection(event.modifiers); + } + } + + Keys.onUpPressed: { + if (positioner.enabled) { + var newIndex = positioner.nearestItem(currentIndex, + FolderTools.effectiveNavDirection(gridView.flow, gridView.effectiveLayoutDirection, Qt.UpArrow)); + + if (newIndex !== -1) { + currentIndex = newIndex; + updateSelection(event.modifiers); + } + } else { + var oldIndex = currentIndex; + + moveCurrentIndexUp(); + + if (oldIndex === currentIndex) { + return; + } + + updateSelection(event.modifiers); + } + } + + Keys.onDownPressed: { + if (positioner.enabled) { + var newIndex = positioner.nearestItem(currentIndex, + FolderTools.effectiveNavDirection(gridView.flow, gridView.effectiveLayoutDirection, Qt.DownArrow)); + + if (newIndex !== -1) { + currentIndex = newIndex; + updateSelection(event.modifiers); + } + } else { + var oldIndex = currentIndex; + + moveCurrentIndexDown(); + + if (oldIndex === currentIndex) { + return; + } + + updateSelection(event.modifiers); + } + } + + function cancelAutoscroll() { + scrollLeft = false; + scrollRight = false; + scrollUp = false; + scrollDown = false; + } + + function rectangleSelect(x, y, width, height) { + var rows = (gridView.flow == GridView.FlowLeftToRight); + var axis = rows ? gridView.width : gridView.height; + var step = rows ? cellWidth : cellHeight; + var perStripe = Math.floor(axis / step); + var stripes = Math.ceil(gridView.count / perStripe); + var cWidth = gridView.cellWidth - (2 * Meui.Units.smallSpacing); + var cHeight = gridView.cellHeight - (2 * Meui.Units.smallSpacing); + var midWidth = gridView.cellWidth / 2; + var midHeight = gridView.cellHeight / 2; + var indices = []; + + for (var s = 0; s < stripes; s++) { + for (var i = 0; i < perStripe; i++) { + var index = (s * perStripe) + i; + + if (index >= gridView.count) { + break; + } + + if (positioner.isBlank(index)) { + continue; + } + + var itemX = ((rows ? i : s) * gridView.cellWidth); + var itemY = ((rows ? s : i) * gridView.cellHeight); + + if (gridView.effectiveLayoutDirection == Qt.RightToLeft) { + itemX -= (rows ? gridView.contentX : gridView.originX); + itemX += cWidth; + itemX = (rows ? gridView.width : gridView.contentItem.width) - itemX; + } + + // Check if the rubberband intersects this cell first to avoid doing more + // expensive work. + if (control.rubberBand.intersects(Qt.rect(itemX + Meui.Units.smallSpacing, itemY + Meui.Units.smallSpacing, + cWidth, cHeight))) { + var item = gridView.contentItem.childAt(itemX + midWidth, itemY + midHeight); + + // If this is a visible item, check for intersection with the actual + // icon or label rects for better feel. + if (item && item.iconArea) { + var iconRect = Qt.rect(itemX + item.iconArea.x, itemY + item.iconArea.y, + item.iconArea.width, item.iconArea.height); + + if (control.rubberBand.intersects(iconRect)) { + indices.push(index); + continue; + } + + var labelRect = Qt.rect(itemX + item.labelArea.x, itemY + item.labelArea.y, + item.labelArea.width, item.labelArea.height); + + if (control.rubberBand.intersects(labelRect)) { + indices.push(index); + continue; + } + } else { + // Otherwise be content with the cell intersection. + indices.push(index); + } + } + } + } + + gridView.cachedRectangleSelection = indices; + } + } + + FM.FolderModel { + id: dir + + sortDirsFirst: true + parseDesktopFiles: true + viewAdapter: viewAdapter + url: dir.desktopPath() + previews: true + previewPlugins: [] + + onListingCompleted: { + if (!gridView.model) { + gridView.model = positioner; + gridView.currentIndex = -1 + } + } + + onMove: { + var rows = (gridView.flow == GridView.FlowLeftToRight); + var axis = rows ? gridView.width : gridView.height; + var step = rows ? cellWidth : cellHeight; + var perStripe = Math.floor(axis / step); + var dropPos = mapToItem(gridView.contentItem, x, y); + var leftEdge = Math.min(gridView.contentX, gridView.originX); + + var moves = [] + var itemX = -1; + var itemY = -1; + var col = -1; + var row = -1; + var from = -1; + var to = -1; + + for (var i = 0; i < urls.length; i++) { + from = positioner.indexForUrl(urls[i]); + to = -1; + + if (from === -1) { + continue; + } + + var offset = dir.dragCursorOffset(positioner.map(from)); + + if (offset.x === -1) { + continue; + } + + itemX = dropPos.x + offset.x + (listener.dragX % cellWidth) + (cellWidth / 2); + itemY = dropPos.y + offset.y + (listener.dragY % cellHeight) + gridView.verticalDropHitscanOffset; + + if (gridView.effectiveLayoutDirection == Qt.RightToLeft) { + itemX -= (rows ? gridView.contentX : gridView.originX); + itemX = (rows ? gridView.width : gridView.contentItem.width) - itemX; + } + + col = Math.floor(itemX / gridView.cellWidth); + row = Math.floor(itemY / gridView.cellHeight); + + if ((rows ? col : row) < perStripe) { + to = ((rows ? row : col) * perStripe) + (rows ? col : row); + + if (to < 0) { + return; + } + } + + if (from !== to) { + moves.push(from); + moves.push(to); + } + } + + if (moves.length) { + positioner.move(moves); + gridView.forceLayout(); + } + + previouslySelectedItemIndex = -1; + dir.clearSelection(); + } + } + + FM.Positioner { + id: positioner + folderModel: dir + enabled: true + perStripe: Math.floor(((gridView.flow == GridView.FlowLeftToRight) + ? gridView.width : gridView.height) / ((gridView.flow == GridView.FlowLeftToRight) + ? gridView.cellWidth : gridView.cellHeight)); + } + + FM.ItemViewAdapter { + id: viewAdapter + + adapterView: gridView + adapterModel: positioner + adapterIconSize: gridView.iconSize * 2 + adapterVisibleArea: Qt.rect(gridView.contentX, gridView.contentY, gridView.contentWidth, gridView.contentHeight) + + Component.onCompleted: { + gridView.movementStarted.connect(viewAdapter.viewScrolled); + dir.viewAdapter = viewAdapter; + } + } + + Component { + id: rubberBandObject + + FM.RubberBand { + id: rubberBand + + width: 0 + height: 0 + z: 99999 + color: Meui.Theme.highlightColor + + function close() { + opacityAnimation.restart() + } + + OpacityAnimator { + id: opacityAnimation + target: rubberBand + to: 0 + from: 1 + duration: 150 + + // This easing curve has an elognated start, which works + // better than a standard easing curve for the rubberband + // animation, which fades out fast and is generally of a + // small area. + easing { + bezierCurve: [0.4, 0.0, 1, 1] + type: Easing.Bezier + } + + onFinished: { + rubberBand.visible = false + rubberBand.enabled = false + rubberBand.destroy() + } + } + } + } + + Component { + id: editorComponent + + TextArea { + id: editor + + visible: false + + wrapMode: TextEdit.Wrap + + textMargin: 0 + + horizontalAlignment: TextEdit.AlignHCenter + + property Item targetItem: null + + onTargetItemChanged: { + if (targetItem != null) { + var xy = getXY(); + x = xy[0]; + y = xy[1]; + width = getWidth(); + height = getInitHeight(); + text = targetItem.label.text; + adjustSize(); + editor.select(0, dir.fileExtensionBoundary(positioner.map(targetItem.index))); + if(isPopup) { + flickableItem.contentX = Math.max(flickableItem.contentWidth - contentItem.width, 0); + } else { + flickableItem.contentY = Math.max(flickableItem.contentHeight - contentItem.height, 0); + } + visible = true; + } else { + x: 0 + y: 0 + visible = false; + } + } + + onVisibleChanged: { + if (visible) { + focus = true; + } else { + scrollArea.focus = true; + } + } + + Keys.onPressed: { + switch(event.key) { + case Qt.Key_Return: + case Qt.Key_Enter: + commit(); + break; + case Qt.Key_Escape: + if (targetItem) { + targetItem = null; + event.accepted = true; + } + break; + case Qt.Key_Home: + if (event.modifiers & Qt.ShiftModifier) { + editor.select(0, cursorPosition); + } else { + editor.select(0, 0); + } + event.accepted = true; + break; + case Qt.Key_End: + if (event.modifiers & Qt.ShiftModifier) { + editor.select(cursorPosition, text.length); + } else { + editor.select(text.length, text.length); + } + event.accepted = true; + break; + default: + adjustSize(); + break; + } + } + + Keys.onReleased: { + adjustSize(); + } + + function getXY() { + var pos = control.mapFromItem(targetItem, targetItem.labelArea.x, targetItem.labelArea.y); + var _x, _y; + + _x = targetItem.x + Math.abs(Math.min(gridView.contentX, gridView.originX)); + _x += __style.padding.left; + _x += scrollArea.viewport.x; + if (verticalScrollBarPolicy == Qt.ScrollBarAlwaysOn + && gridView.effectiveLayoutDirection == Qt.RightToLeft) { + _x -= __verticalScrollBar.parent.verticalScrollbarOffset; + } + _y = pos.y + PlasmaCore.Units.smallSpacing - __style.padding.top; + + return([ _x, _y ]); + } + + function getWidth(addWidthVerticalScroller) { + return targetItem.label.parent.width - PlasmaCore.Units.smallSpacing; + } + + function getHeight(addWidthHoriozontalScroller, init) { + var _height; + if (init) { + _height = targetItem.labelArea.height + __style.padding.top + __style.padding.bottom; + } else { + var realHeight = contentHeight + __style.padding.top + __style.padding.bottom; + _height = realHeight; + } + return(_height + (addWidthHoriozontalScroller ? __horizontalScrollBar.parent.horizontalScrollbarOffset : 0)); + } + + function getInitHeight() { + return(getHeight(false, true)); + } + + function adjustSize() { + if (isPopup) { + if (contentWidth + __style.padding.left + __style.padding.right > width) { + visible = true; + horizontalScrollBarPolicy = Qt.ScrollBarAlwaysOn; + height = getHeight(true); + } else { + horizontalScrollBarPolicy = Qt.ScrollBarAlwaysOff; + height = getHeight(); + } + } else { + height = getHeight(); + if(contentHeight + __style.padding.top + __style.padding.bottom > height) { + visible = true; + verticalScrollBarPolicy = Qt.ScrollBarAlwaysOn; + width = getWidth(true); + } else { + verticalScrollBarPolicy = Qt.ScrollBarAlwaysOff; + width = getWidth(); + } + } + + var xy = getXY(); + x = xy[0]; + y = xy[1]; + } + + function commit() { + if (targetItem) { + dir.rename(positioner.map(targetItem.index), text); + targetItem = null; + } + } + } + +// Component.onCompleted: { +// dir.requestRename.connect(rename); +// } + } +} diff --git a/qml/Desktop/FolderItemDelegate.qml b/qml/Desktop/FolderItemDelegate.qml new file mode 100644 index 0000000..be3d58b --- /dev/null +++ b/qml/Desktop/FolderItemDelegate.qml @@ -0,0 +1,116 @@ +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 org.kde.plasma.core 2.0 as PlasmaCore + +Item { + id: main + + property int index: model.index + property string name: model.blank ? "" : model.display + property bool blank: model.blank + property bool isDir: model.blank ? false : model.isDir + property bool selected: model.blank ? false : model.selected + property Item frame: contentItem + property Item iconArea: icon + property Item labelArea: label + + property bool hovered: (main.GridView.view.hoveredItem === main) + + property color hoveredColor: Qt.rgba(Meui.Theme.highlightColor.r, + Meui.Theme.highlightColor.g, + Meui.Theme.highlightColor.b, 0.1) + property color selectedColor: Qt.rgba(Meui.Theme.highlightColor.r, + Meui.Theme.highlightColor.g, + Meui.Theme.highlightColor.b, 0.9) + + Accessible.name: name + Accessible.role: Accessible.Canvas + + onSelectedChanged: { + if (selected && !blank) { + contentItem.grabToImage(function(result) { + dir.addItemDragImage(positioner.map(index), main.x + contentItem.x, main.y + contentItem.y, contentItem.width, contentItem.height, result.image); + }); + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: Meui.Units.largeSpacing + radius: Meui.Theme.bigRadius + color: selected ? selectedColor : main.hovered ? hoveredColor : "transparent" + + border.color: Qt.rgba(Meui.Theme.highlightColor.r, + Meui.Theme.highlightColor.g, + Meui.Theme.highlightColor.b, 0.3) + border.width: main.hovered || selected ? 1 : 0 + } + + Item { + id: contentItem + anchors.fill: parent + anchors.margins: Meui.Units.largeSpacing + + PlasmaCore.IconItem { + id: icon + z: 2 + + anchors.top: parent.top + anchors.topMargin: Meui.Units.smallSpacing + anchors.horizontalCenter: parent.horizontalCenter + + height: main.height * 0.55 + width: height + + animated: false + usesPlasmaTheme: false + smooth: true + source: model.blank ? "" : model.decoration + overlays: model.blank ? "" : model.overlays + } + + Label { + id: label + z: 2 + + anchors.top: icon.bottom + anchors.topMargin: Meui.Units.smallSpacing + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + width: parent.width + + textFormat: Text.PlainText + + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignTop + + wrapMode: Text.Wrap + elide: Text.ElideRight + color: "#FFFFFF" + opacity: model.isHidden ? 0.6 : 1 + text: model.blank ? "" : model.display + font.italic: model.isLink + } + + DropShadow { + anchors.fill: label + z: 1 + horizontalOffset: 1 + verticalOffset: 1 + radius: Math.round(4 * Meui.Units.devicePixelRatio) + samples: radius * 2 + 1 + spread: 0.35 + color: "black" + opacity: model.isHidden ? 0.4 : 0.5 + source: label + visible: !selected + } + } +} diff --git a/qml/Desktop/FolderTools.js b/qml/Desktop/FolderTools.js new file mode 100644 index 0000000..2e8569d --- /dev/null +++ b/qml/Desktop/FolderTools.js @@ -0,0 +1,43 @@ +function effectiveNavDirection(flow, layoutDirection, direction) { + if (direction === Qt.LeftArrow) { + if (flow === GridView.FlowLeftToRight) { + if (layoutDirection === Qt.LeftToRight) { + return Qt.LeftArrow; + } else { + return Qt.RightArrow; + } + } else { + if (layoutDirection === Qt.LeftToRight) { + return Qt.UpArrow; + } else { + return Qt.DownArrow; + } + } + } else if (direction === Qt.RightArrow) { + if (flow === GridView.FlowLeftToRight) { + if (layoutDirection === Qt.LeftToRight) { + return Qt.RightArrow; + } else { + return Qt.LeftArrow; + } + } else { + if (layoutDirection === Qt.LeftToRight) { + return Qt.DownArrow; + } else { + return Qt.UpArrow; + } + } + } else if (direction === Qt.UpArrow) { + if (flow === GridView.FlowLeftToRight) { + return Qt.UpArrow; + } else { + return Qt.LeftArrow; + } + } else if (direction === Qt.DownArrow) { + if (flow === GridView.FlowLeftToRight) { + return Qt.DownArrow; + } else { + return Qt.RightArrow + } + } +} diff --git a/qml/Desktop/FolderViewDropArea.qml b/qml/Desktop/FolderViewDropArea.qml new file mode 100644 index 0000000..c82118e --- /dev/null +++ b/qml/Desktop/FolderViewDropArea.qml @@ -0,0 +1,53 @@ +import QtQuick 2.12 +import MeuiKit 1.0 as Meui +import org.kde.draganddrop 2.0 as DragDrop + +DragDrop.DropArea { + id: dropArea + + property Item folderView: null + + function handleDragMove(folderView, pos) { + // Trigger autoscroll. + folderView.scrollLeft = (pos.x < (Meui.Units.largeSpacing * 3)); + folderView.scrollRight = (pos.x > width - (Meui.Units.largeSpacing * 3)); + folderView.scrollUp = (pos.y < (Meui.Units.largeSpacing * 3)); + folderView.scrollDown = (pos.y > height - (Meui.Units.largeSpacing * 3)); + + folderView.handleDragMove(pos.x, pos.y); + } + + function handleDragEnd(folderView) { + // Cancel autoscroll. + folderView.scrollLeft = false; + folderView.scrollRight = false; + folderView.scrollUp = false; + folderView.scrollDown = false; + + folderView.endDragMove(); + } + + onDragMove: { + // TODO: We should reject drag moves onto file items that don't accept drops + // (cf. QAbstractItemModel::flags() here, but DeclarativeDropArea currently + // is currently incapable of rejecting drag events. + + if (folderView) { + handleDragMove(folderView, mapToItem(folderView, event.x, event.y)); + } + } + + onDragLeave: { + if (folderView) { + handleDragEnd(folderView); + } + } + + onDrop: { + if (folderView) { + handleDragEnd(folderView); + + folderView.drop(folderView, event, mapToItem(folderView, event.x, event.y)); + } + } +} diff --git a/qml/Dialogs/PropertiesDialog.qml b/qml/Dialogs/PropertiesDialog.qml new file mode 100644 index 0000000..a501fc9 --- /dev/null +++ b/qml/Dialogs/PropertiesDialog.qml @@ -0,0 +1,174 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import MeuiKit 1.0 as Meui + +Window { + id: control + title: qsTr("Properties") + flags: Qt.Dialog | Qt.WindowStaysOnTopHint + + visible: true + + onVisibleChanged: { + if (visible) updateWindowSize() + } + + Component.onCompleted: { + updateWindowSize() + } + + function updateWindowSize() { + if (visible) { + control.width = _mainLayout.implicitWidth + _mainLayout.anchors.leftMargin + _mainLayout.anchors.rightMargin + control.height = _mainLayout.implicitHeight + _mainLayout.anchors.topMargin + _mainLayout.anchors.bottomMargin + control.minimumWidth = control.width + control.minimumHeight = control.height + control.maximumWidth = control.width + control.maximumHeight = control.height + + if (_textField.enabled) + _textField.forceActiveFocus() + } + } + + Item { + id: _contentItem + anchors.fill: parent + focus: true + + Keys.enabled: true + Keys.onEscapePressed: control.close() + + ColumnLayout { + id: _mainLayout + anchors.fill: parent + anchors.leftMargin: Meui.Units.largeSpacing * 2 + anchors.rightMargin: Meui.Units.largeSpacing * 2 + anchors.topMargin: Meui.Units.largeSpacing + anchors.bottomMargin: Meui.Units.largeSpacing + spacing: Meui.Units.largeSpacing + + RowLayout { + spacing: Meui.Units.largeSpacing * 2 + + Image { + width: 64 + height: width + sourceSize: Qt.size(width, height) + source: "image://icontheme/" + main.iconName + } + + TextField { + id: _textField + text: main.fileName + focus: true + Layout.fillWidth: true + Keys.onEscapePressed: control.close() + enabled: !main.multiple + } + } + + GridLayout { + columns: 2 + columnSpacing: Meui.Units.largeSpacing + rowSpacing: Meui.Units.largeSpacing + Layout.alignment: Qt.AlignTop + + onHeightChanged: updateWindowSize() + + Label { + text: qsTr("Type:") + Layout.alignment: Qt.AlignRight + visible: mimeType.visible + } + + Label { + id: mimeType + text: main.mimeType + visible: text + } + + Label { + text: qsTr("Location:") + Layout.alignment: Qt.AlignRight + } + + Label { + id: location + text: main.location + } + + Label { + text: qsTr("Size:") + Layout.alignment: Qt.AlignRight + visible: size.visible + } + + Label { + id: size + text: main.size + visible: text + } + + Label { + text: qsTr("Created:") + Layout.alignment: Qt.AlignRight + visible: creationTime.visible + } + + Label { + id: creationTime + text: main.creationTime + visible: text + } + + Label { + text: qsTr("Modified:") + Layout.alignment: Qt.AlignRight + visible: modifiedTime.visible + } + + Label { + id: modifiedTime + text: main.modifiedTime + visible: text + } + + Label { + text: qsTr("Accessed:") + Layout.alignment: Qt.AlignRight + visible: accessTime.visible + } + + Label { + id: accessTime + text: main.accessedTime + visible: text + } + } + + Item { + height: Meui.Units.largeSpacing + } + + RowLayout { + Layout.alignment: Qt.AlignRight + spacing: Meui.Units.largeSpacing + + Button { + text: qsTr("Cancel") + Layout.fillWidth: true + onClicked: control.close() + } + + Button { + text: qsTr("OK") + Layout.fillWidth: true + onClicked: control.close() + } + } + } + } +} diff --git a/qml/FolderIconDelegate.qml b/qml/FolderIconDelegate.qml new file mode 100644 index 0000000..5a9dd61 --- /dev/null +++ b/qml/FolderIconDelegate.qml @@ -0,0 +1,102 @@ +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 org.kde.plasma.core 2.0 as PlasmaCore + +Item { + id: main + + property int index: model.index + property string name: model.blank ? "" : model.display + property bool blank: model.blank + property bool isDir: model.blank ? false : model.isDir + property bool selected: model.blank ? false : model.selected + property Item frame: contentItem + property Item iconArea: icon + property Item labelArea: label + + property bool hovered: (main.GridView.view.hoveredItem === main) + + property color hoveredColor: Qt.rgba(Meui.Theme.highlightColor.r, + Meui.Theme.highlightColor.g, + Meui.Theme.highlightColor.b, 0.1) + property color selectedColor: Qt.rgba(Meui.Theme.highlightColor.r, + Meui.Theme.highlightColor.g, + Meui.Theme.highlightColor.b, 0.9) + + Accessible.name: name + Accessible.role: Accessible.Canvas + + onSelectedChanged: { + if (selected && !blank) { + contentItem.grabToImage(function(result) { + dirModel.addItemDragImage(positioner.map(index), main.x + contentItem.x, main.y + contentItem.y, contentItem.width, contentItem.height, result.image); + }); + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: Meui.Units.largeSpacing + radius: Meui.Theme.bigRadius + color: selected ? selectedColor : main.hovered ? hoveredColor : "transparent" + + border.color: Qt.rgba(Meui.Theme.highlightColor.r, + Meui.Theme.highlightColor.g, + Meui.Theme.highlightColor.b, 0.3) + border.width: main.hovered || selected ? 1 : 0 + } + + Item { + id: contentItem + anchors.fill: parent + anchors.margins: Meui.Units.largeSpacing + + PlasmaCore.IconItem { + id: icon + z: 2 + + anchors.top: parent.top + anchors.topMargin: Meui.Units.smallSpacing + anchors.horizontalCenter: parent.horizontalCenter + + height: main.height * 0.55 + width: height + + animated: false + usesPlasmaTheme: false + smooth: true + source: model.blank ? "" : model.decoration + overlays: model.blank ? "" : model.overlays + } + + Label { + id: label + z: 2 + + anchors.top: icon.bottom + anchors.topMargin: Meui.Units.smallSpacing + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + width: parent.width + + textFormat: Text.PlainText + + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignTop + + wrapMode: Text.Wrap + elide: Text.ElideRight + color: selected ? Meui.Theme.highlightedTextColor : Meui.Theme.textColor + opacity: model.isHidden ? 0.6 : 1 + text: model.blank ? "" : model.display + font.italic: model.isLink + } + } +} diff --git a/qml/FolderIconView.qml b/qml/FolderIconView.qml new file mode 100644 index 0000000..ff30246 --- /dev/null +++ b/qml/FolderIconView.qml @@ -0,0 +1,381 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +import MeuiKit 1.0 as Meui +import Cutefish.FileManager 1.0 as FM + +GridView { + id: control + + // XXXX + property var iconSize: 130 + Meui.Units.largeSpacing + + cellWidth: { + var extraWidth = calcExtraSpacing(iconSize, control.width - leftMargin - rightMargin); + return iconSize + extraWidth; + } + + cellHeight: { + var extraHeight = calcExtraSpacing(iconSize, control.height - topMargin - bottomMargin); + return iconSize + extraHeight; + } + + ScrollBar.vertical: ScrollBar {} + clip: true + + property Item rubberBand: null + + property Item hoveredItem: null + property Item pressedItem: null + + property int pressX: -1 + property int pressY: -1 + property int dragX: -1 + property int dragY: -1 + property variant cPress: null + property bool doubleClickInProgress: false + + property int anchorIndex: 0 + property bool ctrlPressed: false + property bool shiftPressed: false + + property bool overflowing: (visibleArea.heightRatio < 1.0 || visibleArea.widthRatio < 1.0) + + property bool scrollLeft: false + property bool scrollRight: false + property bool scrollUp: false + property bool scrollDown: false + + property variant cachedRectangleSelection: null + property int previouslySelectedItemIndex: -1 + property int verticalDropHitscanOffset: 0 + + flow: GridView.FlowLeftToRight + + currentIndex: -1 + + onPressXChanged: { + cPress = mapToItem(control.contentItem, pressX, pressY); + } + + onPressYChanged: { + cPress = mapToItem(control.contentItem, pressX, pressY); + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + // propagateComposedEvents: true + hoverEnabled: true + z: -1 + + onPressed: { + control.forceActiveFocus() + + if (mouse.source === Qt.MouseEventSynthesizedByQt) { + var index = control.indexAt(mouse.x, mouse.y) + var indexItem = control.itemAtIndex(index) + if (indexItem && indexItem.iconArea) { + control.currentIndex = index + hoveredItem = indexItem + } else { + hoveredItem = null + } + } + + pressX = mouse.x + pressY = mouse.y + + if (!hoveredItem || hoveredItem.blank) { + if (!control.ctrlPressed) { + control.currentIndex = -1; + previouslySelectedItemIndex = -1; + dirModel.clearSelection(); + } + + if (mouse.buttons & Qt.RightButton) { + clearPressState() + dirModel.openContextMenu(null, mouse.modifiers) + mouse.accepted = true + } + } else { + pressedItem = hoveredItem; + + if (control.shiftPressed && control.currentIndex !== -1) { + positioner.setRangeSelected(control.anchorIndex, hoveredItem.index); + } else { + if (!control.ctrlPressed && !dirModel.isSelected(positioner.map(hoveredItem.index))) { + previouslySelectedItemIndex = -1; + dirModel.clearSelection(); + } + + if (control.ctrlPressed) { + dirModel.toggleSelected(positioner.map(hoveredItem.index)); + } else { + dirModel.setSelected(positioner.map(hoveredItem.index)); + } + + control.currentIndex = hoveredItem.index; + + if (mouse.buttons & Qt.RightButton) { + clearPressState(); + + dirModel.openContextMenu(null, mouse.modifiers); + mouse.accepted = true; + } + } + } + } + + onPositionChanged: { + control.ctrlPressed = (mouse.modifiers & Qt.ControlModifier); + control.shiftPressed = (mouse.modifiers & Qt.ShiftModifier); + + var cPos = mapToItem(control.contentItem, mouse.x, mouse.y); + var item = control.itemAt(cPos.x, cPos.y); + var leftEdge = Math.min(control.contentX, control.originX); + + if (!item || item.blank) { + if (control.hoveredItem && !root.containsDrag) { + control.hoveredItem = null; + } + } else { + var fPos = mapToItem(item, mouse.x, mouse.y); + + if (fPos.x < 0 || fPos.y < 0 || fPos.x > item.width || fPos.y > item.height) { + control.hoveredItem = null; + } else { + control.hoveredItem = item + } + } + + // Trigger autoscroll. + if (pressX != -1) { + control.scrollLeft = (mouse.x <= 0 && control.contentX > leftEdge); + control.scrollRight = (mouse.x >= control.width + && control.contentX < control.contentItem.width - control.width); + control.scrollUp = (mouse.y <= 0 && control.contentY > 0); + control.scrollDown = (mouse.y >= control.height + && control.contentY < control.contentItem.height - control.height); + } + + // Update rubberband geometry. + if (control.rubberBand) { + var rB = control.rubberBand; + + if (cPos.x < cPress.x) { + rB.x = Math.max(leftEdge, cPos.x); + rB.width = Math.abs(rB.x - cPress.x); + } else { + rB.x = cPress.x; + var ceil = Math.max(control.width, control.contentItem.width) + leftEdge; + rB.width = Math.min(ceil - rB.x, Math.abs(rB.x - cPos.x)); + } + + if (cPos.y < cPress.y) { + rB.y = Math.max(0, cPos.y); + rB.height = Math.abs(rB.y - cPress.y); + } else { + rB.y = cPress.y; + var ceil = Math.max(control.height, control.contentItem.height); + rB.height = Math.min(ceil - rB.y, Math.abs(rB.y - cPos.y)); + } + + // Ensure rubberband is at least 1px in size or else it will become + // invisible and not match any items. + rB.width = Math.max(1, rB.width); + rB.height = Math.max(1, rB.height); + + control.rectangleSelect(rB.x, rB.y, rB.width, rB.height); + + return; + } + + // Drag initiation. + if (pressX != -1 /*&& root.isDrag(pressX, pressY, mouse.x, mouse.y)*/) { + if (pressedItem != null && dirModel.isSelected(positioner.map(pressedItem.index))) { + dragX = mouse.x; + dragY = mouse.y; + control.verticalDropHitscanOffset = pressedItem.iconArea.y + (pressedItem.iconArea.height / 2) + dirModel.dragSelected(mouse.x, mouse.y); + dragX = -1; + dragY = -1; + clearPressState(); + } else { + dirModel.pinSelection(); + control.rubberBand = rubberBandObject.createObject(control.contentItem, {x: cPress.x, y: cPress.y}) + control.interactive = false; + } + } + } + + onContainsMouseChanged: { + if (!containsMouse && !control.rubberBand) { + clearPressState(); + + if (control.hoveredItem) { + control.hoveredItem = null; + } + } + } + + onCanceled: pressCanceled() + onReleased: pressCanceled() + } + + function calcExtraSpacing(cellSize, containerSize) { + var availableColumns = Math.floor(containerSize / cellSize); + var extraSpacing = 0; + if (availableColumns > 0) { + var allColumnSize = availableColumns * cellSize; + var extraSpace = Math.max(containerSize - allColumnSize, 0); + extraSpacing = extraSpace / availableColumns; + } + return Math.floor(extraSpacing); + } + + function clearPressState() { + pressedItem = null; + pressX = -1; + pressY = -1; + } + + function rectangleSelect(x, y, width, height) { + var rows = (control.flow === GridView.FlowLeftToRight); + var axis = rows ? control.width : control.height; + var step = rows ? cellWidth : cellHeight; + var perStripe = Math.floor(axis / step); + var stripes = Math.ceil(control.count / perStripe); + var cWidth = control.cellWidth - (2 * Meui.Units.smallSpacing); + var cHeight = control.cellHeight - (2 * Meui.Units.smallSpacing); + var midWidth = control.cellWidth / 2; + var midHeight = control.cellHeight / 2; + var indices = []; + + for (var s = 0; s < stripes; s++) { + for (var i = 0; i < perStripe; i++) { + var index = (s * perStripe) + i; + + if (index >= control.count) { + break; + } + + if (positioner.isBlank(index)) { + continue; + } + + var itemX = ((rows ? i : s) * control.cellWidth); + var itemY = ((rows ? s : i) * control.cellHeight); + + if (control.effectiveLayoutDirection == Qt.RightToLeft) { + itemX -= (rows ? control.contentX : control.originX); + itemX += cWidth; + itemX = (rows ? control.width : control.contentItem.width) - itemX; + } + + // Check if the rubberband intersects this cell first to avoid doing more + // expensive work. + if (control.rubberBand.intersects(Qt.rect(itemX + Meui.Units.smallSpacing, itemY + Meui.Units.smallSpacing, + cWidth, cHeight))) { + var item = control.contentItem.childAt(itemX + midWidth, itemY + midHeight); + + // If this is a visible item, check for intersection with the actual + // icon or label rects for better feel. + if (item && item.iconArea) { + var iconRect = Qt.rect(itemX + item.iconArea.x, itemY + item.iconArea.y, + item.iconArea.width, item.iconArea.height); + + if (control.rubberBand.intersects(iconRect)) { + indices.push(index); + continue; + } + + var labelRect = Qt.rect(itemX + item.labelArea.x, itemY + item.labelArea.y, + item.labelArea.width, item.labelArea.height); + + if (control.rubberBand.intersects(labelRect)) { + indices.push(index); + continue; + } + } else { + // Otherwise be content with the cell intersection. + indices.push(index); + } + } + } + } + + control.cachedRectangleSelection = indices; + } + + onCachedRectangleSelectionChanged: { + if (cachedRectangleSelection == null) { + return; + } + + if (cachedRectangleSelection.length) { + // Set current index to start of selection. + // cachedRectangleSelection is pre-sorted. + currentIndex = cachedRectangleSelection[0]; + } + + dirModel.updateSelection(cachedRectangleSelection.map(positioner.map), control.ctrlPressed); + } + + function pressCanceled() { + if (control.rubberBand) { + control.rubberBand.close() + control.rubberBand = null + + control.interactive = true; + control.cachedRectangleSelection = null; + dirModel.unpinSelection(); + } + + clearPressState(); + control.cancelAutoscroll(); + } + + function cancelAutoscroll() { + scrollLeft = false; + scrollRight = false; + scrollUp = false; + scrollDown = false; + } + + Component { + id: rubberBandObject + + FM.RubberBand { + id: rubberBand + + width: 0 + height: 0 + z: 99999 + color: Meui.Theme.highlightColor + + function close() { + opacityAnimation.restart() + } + + OpacityAnimator { + id: opacityAnimation + target: rubberBand + to: 0 + from: 1 + duration: 150 + + easing { + bezierCurve: [0.4, 0.0, 1, 1] + type: Easing.Bezier + } + + onFinished: { + rubberBand.visible = false + rubberBand.enabled = false + rubberBand.destroy() + } + } + } + } +} diff --git a/qml/FolderListDelegate.qml b/qml/FolderListDelegate.qml new file mode 100644 index 0000000..f556d26 --- /dev/null +++ b/qml/FolderListDelegate.qml @@ -0,0 +1,79 @@ +import QtQuick 2.4 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.0 + +import Cutefish.FileManager 1.0 +import MeuiKit 1.0 as Meui +import org.kde.plasma.core 2.0 as PlasmaCore + +Item { + id: control + + property int index: model.index + property string name: model.blank ? "" : model.display + property bool blank: model.blank + property bool isDir: model.blank ? false : model.isDir + property bool selected: model.blank ? false : model.selected + property Item frame: contentItem + property Item iconArea: iconItem + property Item labelArea: label1 + + property color hoveredColor: Qt.rgba(Meui.Theme.textColor.r, + Meui.Theme.textColor.g, + Meui.Theme.textColor.b, 0.1) + + Accessible.name: name + Accessible.role: Accessible.Canvas + + MouseArea { + id: _mouseArea + anchors.fill: parent + hoverEnabled: true + } + + Rectangle { + z: -1 + anchors.fill: parent + radius: Meui.Theme.bigRadius + color: selected ? Meui.Theme.highlightColor : _mouseArea.containsMouse ? control.hoveredColor : "transparent" + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Meui.Units.smallSpacing + anchors.rightMargin: Meui.Units.smallSpacing + spacing: Meui.Units.largeSpacing + + Item { + id: iconItem + Layout.fillHeight: true + width: parent.height * 0.8 + + PlasmaCore.IconItem { + id: icon + z: 2 + + anchors.fill: parent + + animated: false + usesPlasmaTheme: false + smooth: true + source: model.blank ? "" : model.decoration + overlays: model.blank ? "" : model.overlays + } + } + + Label { + id: label1 + Layout.fillWidth: true + text: name + color: selected ? Meui.Theme.highlightedTextColor : Meui.Theme.textColor + } + + Label { + id: label2 + color: selected ? Meui.Theme.highlightedTextColor : Meui.Theme.textColor + } + } +} diff --git a/qml/FolderListView.qml b/qml/FolderListView.qml new file mode 100644 index 0000000..a34a855 --- /dev/null +++ b/qml/FolderListView.qml @@ -0,0 +1,38 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import MeuiKit 1.0 as Meui +import Cutefish.FileManager 1.0 as FM + +ListView { + id: control + + signal clicked(var mouse) + signal positionChanged(var mouse) + signal pressed(var mouse) + signal released(var mouse) + + ScrollBar.vertical: ScrollBar {} + spacing: Meui.Units.largeSpacing + clip: true + + snapMode: ListView.NoSnap + + highlightFollowsCurrentItem: true + highlightMoveDuration: 0 + highlightResizeDuration : 0 + + MouseArea { + anchors.fill: parent + z: -1 + hoverEnabled: true + propagateComposedEvents: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: parent.clicked(mouse) + onPositionChanged: parent.positionChanged(mouse) + onPressed: parent.pressed(mouse) + onReleased: parent.released(mouse) + Keys.forwardTo: _listViewBrowser + } +} diff --git a/qml/GlobalSettings.qml b/qml/GlobalSettings.qml new file mode 100644 index 0000000..f0fca51 --- /dev/null +++ b/qml/GlobalSettings.qml @@ -0,0 +1,9 @@ +import QtQuick 2.0 +import Qt.labs.settings 1.0 + +Settings { + property int viewMethod: 0 // 0 = Grid, 1 = List + property bool showHidden: false + property int width: 1080 + property int height: 645 +} diff --git a/qml/IconButton.qml b/qml/IconButton.qml new file mode 100644 index 0000000..99e3857 --- /dev/null +++ b/qml/IconButton.qml @@ -0,0 +1,39 @@ +import QtQuick 2.12 +import MeuiKit 1.0 as Meui + +Item { + id: control + width: 24 + height: 24 + + property alias source: _image.source + property color hoveredColor: Meui.Theme.darkMode ? Qt.lighter(Meui.Theme.backgroundColor, 1.1) + : Qt.darker(Meui.Theme.backgroundColor, 1.2) + property color pressedColor: Meui.Theme.darkMode ? Qt.lighter(Meui.Theme.backgroundColor, 1.2) + : Qt.darker(Meui.Theme.backgroundColor, 1.3) + + signal clicked() + + Rectangle { + id: _background + anchors.fill: parent + radius: Meui.Theme.smallRadius + color: _mouseArea.pressed ? pressedColor : _mouseArea.containsMouse ? control.hoveredColor : Meui.Theme.backgroundColor + } + + Image { + id: _image + anchors.centerIn: parent + width: control.height * 0.64 + height: width + sourceSize: Qt.size(width, height) + } + + MouseArea { + id: _mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onClicked: control.clicked() + } +} diff --git a/qml/IconDelegate.qml b/qml/IconDelegate.qml new file mode 100644 index 0000000..168d12c --- /dev/null +++ b/qml/IconDelegate.qml @@ -0,0 +1,102 @@ +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 org.kde.plasma.core 2.0 as PlasmaCore + +Item { + id: main + + property int index: model.index + property string name: model.blank ? "" : model.display + property bool blank: model.blank + property bool isDir: model.blank ? false : model.isDir + property bool selected: model.blank ? false : model.selected + property Item frame: contentItem + property Item iconArea: icon + property Item labelArea: label + + property bool hovered: (main.GridView.view.hoveredItem === main) + + property color hoveredColor: Qt.rgba(Meui.Theme.highlightColor.r, + Meui.Theme.highlightColor.g, + Meui.Theme.highlightColor.b, 0.1) + property color selectedColor: Qt.rgba(Meui.Theme.highlightColor.r, + Meui.Theme.highlightColor.g, + Meui.Theme.highlightColor.b, 0.9) + + Accessible.name: name + Accessible.role: Accessible.Canvas + + onSelectedChanged: { + if (selected && !blank) { + contentItem.grabToImage(function(result) { + dir.addItemDragImage(positioner.map(index), main.x + contentItem.x, main.y + contentItem.y, contentItem.width, contentItem.height, result.image); + }); + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: Meui.Units.largeSpacing + radius: Meui.Theme.bigRadius + color: selected ? selectedColor : main.hovered ? hoveredColor : "transparent" + + border.color: Qt.rgba(Meui.Theme.highlightColor.r, + Meui.Theme.highlightColor.g, + Meui.Theme.highlightColor.b, 0.3) + border.width: main.hovered || selected ? 1 : 0 + } + + Item { + id: contentItem + anchors.fill: parent + anchors.margins: Meui.Units.largeSpacing + + PlasmaCore.IconItem { + id: icon + z: 2 + + anchors.top: parent.top + anchors.topMargin: Meui.Units.smallSpacing + anchors.horizontalCenter: parent.horizontalCenter + + height: main.height * 0.55 + width: height + + animated: false + usesPlasmaTheme: false + smooth: true + source: model.blank ? "" : model.decoration + overlays: model.blank ? "" : model.overlays + } + + Label { + id: label + z: 2 + + anchors.top: icon.bottom + anchors.topMargin: Meui.Units.smallSpacing + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + width: parent.width + + textFormat: Text.PlainText + + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignTop + + wrapMode: Text.Wrap + elide: Text.ElideRight + color: Meui.Theme.textColor + opacity: model.isHidden ? 0.6 : 1 + text: model.blank ? "" : model.display + font.italic: model.isLink + } + } +} diff --git a/qml/ItemMenu.qml b/qml/ItemMenu.qml new file mode 100644 index 0000000..edf7170 --- /dev/null +++ b/qml/ItemMenu.qml @@ -0,0 +1,101 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import Cutefish.FileManager 1.0 +import MeuiKit 1.0 as Meui + +Menu { + id: control + implicitWidth: 200 + + property var item : ({}) + property int index : -1 + property bool isDir : false + property bool isExec : false + + signal openClicked(var item) + signal removeClicked(var item) + signal copyClicked(var item) + signal cutClicked(var item) + signal renameClicked(var item) + signal wallpaperClicked(var item) + signal propertiesClicked(var item) + + MenuItem { + text: qsTr("Open") + onTriggered: { + openClicked(control.item) + close() + } + } + + MenuItem { + text: qsTr("Copy") + onTriggered: { + copyClicked(control.item) + close() + } + } + + MenuItem { + text: qsTr("Cut") + onTriggered: { + cutClicked(control.item) + close() + } + } + + MenuItem { + text: qsTr("Move to Trash") + onTriggered: { + removeClicked(control.item) + close() + } + } + + MenuSeparator {} + + MenuItem { + text: qsTr("Rename") + onTriggered: { + renameClicked(control.item) + close() + } + } + + MenuItem { + text: qsTr("Open in Terminal") + } + + MenuItem { + id: wallpaperItem + text: qsTr("Set As Wallpaper") + visible: false + onTriggered: { + wallpaperClicked(control.item) + close() + } + } + + MenuItem { + id: properties + text: qsTr("Properties") + onTriggered: { + propertiesClicked(control.item) + close() + } + } + + function show(index) { + control.item = currentFMModel.get(index) + + if (item) { + control.index = index + control.isDir = item.isdir === true || item.isdir === "true" + control.isExec = item.executable === true || item.executable === "true" + wallpaperItem.visible = item.img === "true" + + popup() + } + + } +} diff --git a/qml/PathBar.qml b/qml/PathBar.qml new file mode 100644 index 0000000..9fe2999 --- /dev/null +++ b/qml/PathBar.qml @@ -0,0 +1,138 @@ +import QtQuick 2.4 +import QtQuick.Controls 2.4 +import QtGraphicalEffects 1.0 +import MeuiKit 1.0 as Meui +import Cutefish.FileManager 1.0 + +Item { + id: control + + property string url: "" + signal placeClicked(string path) + signal pathChanged(string path) + + onUrlChanged: { + _pathList.path = control.url + } + + BaseModel { + id: _pathModel + list: _pathList + } + + PathList { + id: _pathList + } + + Rectangle { + anchors.fill: parent + radius: Meui.Theme.smallRadius + color: Meui.Theme.backgroundColor + } + + ListView { + id: listView + anchors.fill: parent + model: _pathModel + orientation: Qt.Horizontal + layoutDirection: Qt.LeftToRight + clip: true + + spacing: Meui.Units.smallSpacing + + onCountChanged: { + currentIndex = listView.count - 1 + listView.positionViewAtEnd() + } + + delegate: MouseArea { + id: pathBarItem + height: listView.height + width: label.width + Meui.Units.largeSpacing * 2 + + property bool selected: index === listView.count - 1 + property color pressedColor: Qt.rgba(Meui.Theme.textColor.r, + Meui.Theme.textColor.g, + Meui.Theme.textColor.b, 0.5) + + onClicked: control.placeClicked(model.path) + + Rectangle { + anchors.fill: parent + anchors.margins: 2 + color: Meui.Theme.highlightColor + radius: Meui.Theme.smallRadius + visible: selected + + layer.enabled: true + layer.effect: DropShadow { + transparentBorder: true + radius: 2 + samples: 2 + horizontalOffset: 0 + verticalOffset: 0 + color: Qt.rgba(0, 0, 0, 0.1) + } + } + + Label { + id: label + text: model.label + anchors.centerIn: parent + color: selected ? Meui.Theme.highlightedTextColor : pathBarItem.pressed + ? pressedColor : Meui.Theme.textColor + } + } + + MouseArea { + anchors.fill: parent + z: -1 + + onClicked: { + if (!addressEdit.visible) { + openEditor() + } + } + } + } + + TextField { + id: addressEdit + anchors.centerIn: parent + width: parent.width + height: parent.height + visible: false + selectByMouse: true + inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase + + text: control.url + + onAccepted: { + control.pathChanged(text) + closeEditor() + } + + Keys.onPressed: { + if (event.key === Qt.Key_Escape) + focus = false + } + + onActiveFocusChanged: { + if (!activeFocus) { + closeEditor() + } + } + } + + function closeEditor() { + addressEdit.visible = false + listView.visible = true + } + + function openEditor() { + addressEdit.visible = true + addressEdit.forceActiveFocus() + addressEdit.selectAll() + listView.visible = false + } +} diff --git a/qml/SideBar.qml b/qml/SideBar.qml new file mode 100644 index 0000000..d9fe1bb --- /dev/null +++ b/qml/SideBar.qml @@ -0,0 +1,74 @@ +import QtQuick 2.4 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.4 +import MeuiKit 1.0 as Meui +import Cutefish.FileManager 1.0 + +ListView { + id: control + implicitWidth: 200 + + property string currentUrl + + signal placeClicked(string path) + signal itemClicked(int index) + + onItemClicked: { + var item = placesModel.get(index) + control.placeClicked(item.url) + } + + onCurrentUrlChanged: { + syncIndex(currentUrl) + } + + Component.onCompleted: { + syncIndex(currentUrl) + } + + function syncIndex(path) { + control.currentIndex = -1 + + for (var i = 0; i < control.count; ++i) { + if (path === control.model.get(i).url) { + control.currentIndex = i + break + } + } + } + + PlacesModel { + id: placesModel + } + + clip: true + spacing: Meui.Units.smallSpacing + leftMargin: Meui.Units.smallSpacing + rightMargin: Meui.Units.smallSpacing + + model: placesModel + + ScrollBar.vertical: ScrollBar {} + flickableDirection: Flickable.VerticalFlick + + highlightFollowsCurrentItem: true + highlightMoveDuration: 0 + highlightResizeDuration : 0 + + highlight: Rectangle { + radius: Meui.Theme.smallRadius + color: Meui.Theme.highlightColor + } + + delegate: SidebarItem { + id: listItem + checked: control.currentIndex === index + width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin + height: 40 + + onClicked: { + control.currentIndex = index + control.itemClicked(index) + } + } +} diff --git a/qml/SidebarItem.qml b/qml/SidebarItem.qml new file mode 100644 index 0000000..ef5b7aa --- /dev/null +++ b/qml/SidebarItem.qml @@ -0,0 +1,69 @@ +import QtQuick 2.4 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.1 +import QtGraphicalEffects 1.0 + +import MeuiKit 1.0 as Meui + +Item { + id: item + + property bool checked: false + signal clicked + + Rectangle { + id: rect + anchors.fill: parent + radius: Meui.Theme.smallRadius + color: item.checked ? "transparent" + : mouseArea.containsMouse ? Qt.rgba(Meui.Theme.textColor.r, + Meui.Theme.textColor.g, + Meui.Theme.textColor.b, 0.1) : "transparent" + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onClicked: item.clicked() + } + + RowLayout { + anchors.fill: rect + anchors.leftMargin: Meui.Units.largeSpacing + anchors.rightMargin: Meui.Units.largeSpacing + + spacing: Meui.Units.largeSpacing + + Item { + id: iconItem + height: item.height * 0.55 + width: height + + Image { + id: image + anchors.fill: parent + sourceSize: Qt.size(width, height) + source: model.iconPath ? model.iconPath : "image://icontheme/" + model.iconName + Layout.alignment: Qt.AlignVCenter + } + + ColorOverlay { + anchors.fill: parent + source: parent + color: itemTitle.color + visible: Meui.Theme.darkMode && model.iconPath || checked + } + } + + Label { + id: itemTitle + text: model.name + color: item.checked ? Meui.Theme.highlightedTextColor : Meui.Theme.textColor + elide: Text.ElideRight + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + } + } +} diff --git a/qml/main.qml b/qml/main.qml new file mode 100644 index 0000000..245775f --- /dev/null +++ b/qml/main.qml @@ -0,0 +1,106 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.3 + +import Cutefish.FileManager 1.0 + +import MeuiKit 1.0 as Meui + +Meui.Window { + id: root + width: settings.width + height: settings.height + minimumWidth: 900 + minimumHeight: 600 + visible: true + title: qsTr("File Manager") + + hideHeaderOnMaximize: false + headerBarHeight: 35 + Meui.Units.largeSpacing + backgroundColor: Meui.Theme.secondBackgroundColor + + property QtObject settings: GlobalSettings { } + + onClosing: { + settings.width = root.width + settings.height = root.height + } + + headerBar: Item { + RowLayout { + anchors.fill: parent + anchors.leftMargin: Meui.Units.largeSpacing + anchors.rightMargin: Meui.Units.smallSpacing + anchors.topMargin: Meui.Units.largeSpacing + spacing: Meui.Units.smallSpacing + + IconButton { + Layout.fillHeight: true + implicitWidth: height + source: Meui.Theme.darkMode ? "qrc:/images/dark/go-previous.svg" : "qrc:/images/light/go-previous.svg" + onClicked: _browserView.goBack() + } + + IconButton { + Layout.fillHeight: true + implicitWidth: height + source: Meui.Theme.darkMode ? "qrc:/images/dark/go-next.svg" : "qrc:/images/light/go-next.svg" + onClicked: _browserView.goForward() + } + + PathBar { + id: pathBar + Layout.fillWidth: true + Layout.fillHeight: true + url: _browserView.url + onPlaceClicked: _browserView.openFolder(path) + onPathChanged: _browserView.openFolder(path) + } + + IconButton { + Layout.fillHeight: true + implicitWidth: height + source: Meui.Theme.darkMode ? "qrc:/images/dark/grid.svg" : "qrc:/images/light/grid.svg" + onClicked: settings.viewMethod = 1 + } + + IconButton { + Layout.fillHeight: true + implicitWidth: height + source: Meui.Theme.darkMode ? "qrc:/images/dark/list.svg" : "qrc:/images/light/list.svg" + onClicked: settings.viewMethod = 0 + } + } + } + + ColumnLayout { + anchors.fill: parent + spacing: Meui.Units.largeSpacing + + Item { + id: bottomControls + Layout.fillWidth: true + Layout.fillHeight: true + + RowLayout { + anchors.fill: parent + anchors.topMargin: Meui.Units.largeSpacing + spacing: 0 + + SideBar { + Layout.fillHeight: true + currentUrl: _browserView.model.url + onPlaceClicked: _browserView.model.url = path + } + + BrowserView { + id: _browserView + Layout.fillWidth: true + Layout.fillHeight: true + // onOpenPathBar: pathBar.openEditor() + } + } + } + } +} diff --git a/src/baselist.cpp b/src/baselist.cpp new file mode 100644 index 0000000..4847df3 --- /dev/null +++ b/src/baselist.cpp @@ -0,0 +1,86 @@ +/* + * + * Copyright (C) 2019 camilo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "baselist.h" +#include "basemodel.h" + +BaseList::BaseList(QObject *parent) + : QObject(parent) + , m_model(nullptr) +{ +} + +int BaseList::getCount() const +{ + return this->items().count(); +} + +QVariantMap BaseList::get(const int &index) const +{ + if (this->m_model) { + return this->m_model->get(index); + } + + if (index >= 0 && this->items().size() > 0 && index < this->items().size()) { + return FMH::toMap(this->items()[index]); + } + + return QVariantMap(); +} + +FMH::MODEL_LIST BaseList::getItems() const +{ + if (this->m_model && !this->m_model->getFilter().isEmpty()) { + return FMH::toModelList(this->m_model->getAll()); + } + + return this->items(); +} + +int BaseList::mappedIndex(const int &index) const +{ + if (this->m_model) + return this->m_model->mappedToSource(index); + + return index; +} + +int BaseList::mappedIndexFromSource(const int &index) const +{ + if (this->m_model) + return this->m_model->mappedFromSource(index); + + return index; +} + +bool BaseList::exists(const FMH::MODEL_KEY &key, const QString &value) const +{ + return this->indexOf(key, value) >= 0; +} + +int BaseList::indexOf(const FMH::MODEL_KEY &key, const QString &value) const +{ + const auto it = std::find_if(this->items().constBegin(), this->items().constEnd(), [&](const FMH::MODEL &item) -> bool { + return item[key] == value; + }); + + if (it != this->items().constEnd()) + return this->mappedIndexFromSource(std::distance(this->items().constBegin(), it)); + else + return -1; +} diff --git a/src/baselist.h b/src/baselist.h new file mode 100644 index 0000000..43e1fcb --- /dev/null +++ b/src/baselist.h @@ -0,0 +1,89 @@ +/* + * + * Copyright (C) 2019 camilo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef BASELIST_H +#define BASELIST_H + +#include "fmh.h" + +#include + +/** + * @todo write docs + */ +#include + +class BaseModel; +class BaseList : public QObject, public QQmlParserStatus +{ + Q_INTERFACES(QQmlParserStatus) + + Q_OBJECT + Q_PROPERTY(int count READ getCount NOTIFY countChanged FINAL) + +public: + /** + * Default constructor + */ + explicit BaseList(QObject *parent = nullptr); + + virtual const FMH::MODEL_LIST &items() const = 0; + virtual void classBegin() override + { + } + virtual void componentComplete() override + { + } + virtual void modelHooked() {}; + + int getCount() const; + + /** + * @brief getItems + * Get all the items in the list model. If the model has been filtered or sorted those are the items that are returned + * @param index + * @return + */ + FMH::MODEL_LIST getItems() const; + + const BaseModel *m_model; // becarefull this is owned by qml engine, this is only supossed to be a viewer + +public slots: + int mappedIndex(const int &index) const; + int mappedIndexFromSource(const int &index) const; + QVariantMap get(const int &index) const; + +protected: + bool exists(const FMH::MODEL_KEY &key, const QString &value) const; + int indexOf(const FMH::MODEL_KEY &key, const QString &value) const; + +signals: + void preItemAppended(); + void preItemsAppended(uint count); + void postItemAppended(); + void preItemAppendedAt(int index); + void preItemRemoved(int index); + void postItemRemoved(); + void updateModel(int index, QVector roles); + void preListChanged(); + void postListChanged(); + + void countChanged(); +}; + +#endif // BASELIST_H diff --git a/src/basemodel.cpp b/src/basemodel.cpp new file mode 100644 index 0000000..437e2e4 --- /dev/null +++ b/src/basemodel.cpp @@ -0,0 +1,333 @@ +/* + * + * Copyright (C) 2019 camilo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "basemodel.h" +#include "baselist.h" + +BaseModel::BaseModel(QObject *parent) + : QSortFilterProxyModel(parent) + , m_model(new PrivateAbstractListModel(this)) +{ + this->setSourceModel(this->m_model); + this->setDynamicSortFilter(true); +} + +void BaseModel::setFilterString(const QString &string) +{ + this->setFilterCaseSensitivity(Qt::CaseInsensitive); + this->setFilterFixedString(string); + // this->setFilterRegExp(QRegExp(string, Qt::CaseInsensitive)); +} + +void BaseModel::setSortOrder(const int &sortOrder) +{ + this->sort(0, static_cast(sortOrder)); +} + +QVariantMap BaseModel::get(const int &index) const +{ + QVariantMap res; + if (index >= this->rowCount() || index < 0) + return res; + + const auto roleNames = this->roleNames(); + for (const auto &role : roleNames) + res.insert(role, this->index(index, 0).data(FMH::MODEL_NAME_KEY[role]).toString()); + + return res; +} + +QVariantList BaseModel::getAll() const +{ + QVariantList res; + for (auto i = 0; i < this->rowCount(); i++) + res << this->get(i); + + return res; +} + +void BaseModel::setFilter(const QString &filter) +{ + if (this->m_filter == filter) + return; + + this->m_filter = filter; + emit this->filterChanged(this->m_filter); + this->setFilterFixedString(this->m_filter); +} + +const QString BaseModel::getFilter() const +{ + return this->m_filter; +} + +void BaseModel::setSortOrder(const Qt::SortOrder &sortOrder) +{ + if (this->m_sortOrder == sortOrder) + return; + + this->m_sortOrder = sortOrder; + emit this->sortOrderChanged(this->m_sortOrder); + this->sort(0, this->m_sortOrder); +} + +Qt::SortOrder BaseModel::getSortOrder() const +{ + return this->m_sortOrder; +} + +void BaseModel::setSort(const QString &sort) +{ + if (this->m_sort == sort) + return; + + this->m_sort = sort; + emit this->sortChanged(this->m_sort); + this->setSortRole(FMH::MODEL_NAME_KEY[sort]); + this->sort(0, this->m_sortOrder); +} + +QString BaseModel::getSort() const +{ + return this->m_sort; +} + +int BaseModel::mappedFromSource(const int &index) const +{ + return this->mapFromSource(this->m_model->index(index, 0)).row(); +} + +int BaseModel::mappedToSource(const int &index) const +{ + return this->mapToSource(this->index(index, 0)).row(); +} + +bool BaseModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + if (this->filterRole() != Qt::DisplayRole) { + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + const auto data = this->sourceModel()->data(index, this->filterRole()).toString(); + return data.contains(this->filterRegExp()); + } + + const auto roleNames = this->sourceModel()->roleNames(); + for (const auto &role : roleNames) { + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + const auto data = this->sourceModel()->data(index, FMH::MODEL_NAME_KEY[role]).toString(); + if (data.contains(this->filterRegExp())) + return true; + else + continue; + } + + return false; +} + +BaseList *BaseModel::getList() const +{ + return this->m_model->getList(); +} + +BaseList *BaseModel::PrivateAbstractListModel::getList() const +{ + return this->list; +} + +void BaseModel::PrivateAbstractListModel::setList(BaseList *value) +{ + beginResetModel(); + + if (this->list) + this->list->disconnect(this); + + this->list = value; + + if (this->list) { + connect( + this->list, + &BaseList::preItemAppendedAt, + this, + [=](int index) { + beginInsertRows(QModelIndex(), index, index); + }, + Qt::DirectConnection); + + connect( + this->list, + &BaseList::preItemAppended, + this, + [=]() { + const int index = this->list->items().size(); + beginInsertRows(QModelIndex(), index, index); + }, + Qt::DirectConnection); + + connect( + this->list, + &BaseList::preItemsAppended, + this, + [=](uint count) { + const int index = this->list->items().size(); + beginInsertRows(QModelIndex(), index, index + count - 1); + }, + Qt::DirectConnection); + + connect( + this->list, + &BaseList::postItemAppended, + this, + [=]() { + endInsertRows(); + }, + Qt::DirectConnection); + + connect( + this->list, + &BaseList::preItemRemoved, + this, + [=](int index) { + beginRemoveRows(QModelIndex(), index, index); + }, + Qt::DirectConnection); + + connect( + this->list, + &BaseList::postItemRemoved, + this, + [=]() { + endRemoveRows(); + }, + Qt::DirectConnection); + + connect( + this->list, + &BaseList::updateModel, + this, + [=](int index, QVector roles) { + emit this->dataChanged(this->index(index), this->index(index), roles); + }, + Qt::DirectConnection); + + connect( + this->list, + &BaseList::preListChanged, + this, + [=]() { + beginResetModel(); + }, + Qt::DirectConnection); + + connect( + this->list, + &BaseList::postListChanged, + this, + [=]() { + endResetModel(); + }, + Qt::DirectConnection); + } + + endResetModel(); +} + +void BaseModel::setList(BaseList *value) +{ + value->modelHooked(); + this->m_model->setList(value); + this->getList()->m_model = this; + emit this->listChanged(); +} + +BaseModel::PrivateAbstractListModel::PrivateAbstractListModel(BaseModel *model) + : QAbstractListModel(model) + , list(nullptr) + , m_model(model) +{ + connect( + this, + &QAbstractListModel::rowsInserted, + this, + [this](QModelIndex, int, int) { + if (this->list) { + emit this->list->countChanged(); + } + }, + Qt::DirectConnection); + + connect( + this, + &QAbstractListModel::rowsRemoved, + this, + [this](QModelIndex, int, int) { + if (this->list) { + emit this->list->countChanged(); + } + }, + Qt::DirectConnection); +} + +int BaseModel::PrivateAbstractListModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid() || !list) + return 0; + + return list->items().size(); +} + +QVariant BaseModel::PrivateAbstractListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || !list) + return QVariant(); + + auto value = list->items().at(index.row())[static_cast(role)]; + + if (role == FMH::MODEL_KEY::ADDDATE || role == FMH::MODEL_KEY::DATE || role == FMH::MODEL_KEY::MODIFIED || role == FMH::MODEL_KEY::RELEASEDATE) { + const auto date = QDateTime::fromString(value, Qt::TextDate); + if (date.isValid()) + return date; + } + + return value; +} + +bool BaseModel::PrivateAbstractListModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + Q_UNUSED(index); + Q_UNUSED(value); + Q_UNUSED(role); + + return false; +} + +Qt::ItemFlags BaseModel::PrivateAbstractListModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + + return Qt::ItemIsEditable; // FIXME: Implement me! +} + +QHash BaseModel::PrivateAbstractListModel::roleNames() const +{ + QHash names; + + for (const auto &key : FMH::MODEL_NAME.keys()) + names[key] = QString(FMH::MODEL_NAME[key]).toUtf8(); + + return names; +} diff --git a/src/basemodel.h b/src/basemodel.h new file mode 100644 index 0000000..5f7dd87 --- /dev/null +++ b/src/basemodel.h @@ -0,0 +1,181 @@ +/* + * + * Copyright (C) 2019 camilo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef BASEMODEL_H +#define BASEMODEL_H + +#include +#include +#include +#include + +class BaseList; + +/** + * @brief The BaseModel class + * The BaseModel is a template model to use with a BaseList, it aims to be a generic and simple data model to quickly model string based models using the FMH::MODEL_LIST and FMH::MODEL_KEY types. + * + * This type is exposed to QML to quickly create a modle that can be filtered, sorted and has another usefull functionalities. + */ +class BaseModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(BaseList *list READ getList WRITE setList NOTIFY listChanged) + Q_PROPERTY(QString filter READ getFilter WRITE setFilter NOTIFY filterChanged) + Q_PROPERTY(Qt::SortOrder sortOrder READ getSortOrder WRITE setSortOrder NOTIFY sortOrderChanged) + Q_PROPERTY(QString sort READ getSort WRITE setSort NOTIFY sortChanged) + +public: + BaseModel(QObject *parent = nullptr); + + /** + * @brief getList + * The list being handled by the model + * @return + */ + BaseList *getList() const; + + /** + * @brief setList + * For the model to work you need to set a BaseList, by subclassing it and exposing it to the QML engine + * @param value + */ + void setList(BaseList *value); + + /** + * @brief getSortOrder + * The current sort order being applied + * @return + */ + Qt::SortOrder getSortOrder() const; + + /** + * @brief getSort + * The current sorting key + * @return + */ + QString getSort() const; + + /** + * @brief getFilter + * The filter being applied to the model + * @return + */ + const QString getFilter() const; + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + +private: + class PrivateAbstractListModel; + PrivateAbstractListModel *m_model; + QString m_filter; + Qt::SortOrder m_sortOrder; + QString m_sort; + + [[deprecated]] void setFilterString(const QString &string); + + [[deprecated]] void setSortOrder(const int &sortOrder); + +public slots: + /** + * @brief setFilter + * Filter the model using a simple string, to clear the filter just set it to a empty string + * @param filter + * Simple filter string + */ + void setFilter(const QString &filter); + + /** + * @brief setSortOrder + * Set the sort order, asc or dec + * @param sortOrder + */ + void setSortOrder(const Qt::SortOrder &sortOrder); + + /** + * @brief setSort + * Set the sort key. The sort keys can be found in the FMH::MODEL_KEY keys + * @param sort + */ + void setSort(const QString &sort); + + /** + * @brief get + * Returns an item in the model/list. This method correctly maps the given index in case the modle has been sorted or filtered + * @param index + * Index of the item in the list + * @return + */ + QVariantMap get(const int &index) const; + + /** + * @brief getAll + * Returns all the items in the list represented as a QVariantList to be able to be used in QML. This operation performs a transformation from FMH::MODEL_LIST to QVariantList + * @return + * All the items in the list + */ + QVariantList getAll() const; + + /** + * @brief mappedFromSource + * Maps an index from the base list to the model, incase the modle has been filtered or sorted, this gives you the right mapped index + * @param index + * @return + */ + int mappedFromSource(const int &index) const; + + /** + * @brief mappedToSource + * given an index from the filtered or sorted model it return the mapped index to the original list index + * @param index + * @return + */ + int mappedToSource(const int &index) const; +signals: + void listChanged(); + void filterChanged(QString filter); + void sortOrderChanged(Qt::SortOrder sortOrder); + void sortChanged(QString sort); +}; + +class BaseModel::PrivateAbstractListModel : public QAbstractListModel +{ + Q_OBJECT +public: + PrivateAbstractListModel(BaseModel *model); + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + // Editable: + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex &index) const override; + + virtual QHash roleNames() const override; + + BaseList *getList() const; + void setList(BaseList *value); + +private: + BaseList *list; + BaseModel *m_model; +}; + +#endif // BASEMODEL_H diff --git a/src/desktop/desktopsettings.cpp b/src/desktop/desktopsettings.cpp new file mode 100644 index 0000000..3cd5231 --- /dev/null +++ b/src/desktop/desktopsettings.cpp @@ -0,0 +1,55 @@ +#include "desktopsettings.h" + +#include +#include + +DesktopSettings::DesktopSettings(QObject *parent) + : QObject(parent) + , m_interface("org.cutefish.Settings", + "/Theme", "org.cutefish.Theme", + QDBusConnection::sessionBus(), this) +{ + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(this); + watcher->setConnection(QDBusConnection::sessionBus()); + watcher->addWatchedService("org.cutefish.Settings"); + connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &DesktopSettings::init); + + init(); +} + +QString DesktopSettings::wallpaper() const +{ + return m_wallpaper; +} + +bool DesktopSettings::dimsWallpaper() const +{ + return m_interface.property("darkModeDimsWallpaer").toBool(); +} + +void DesktopSettings::launch(const QString &command, const QStringList &args) +{ + QProcess process; + process.setProgram(command); + process.setArguments(args); + process.startDetached(); +} + +void DesktopSettings::init() +{ + if (m_interface.isValid()) { + connect(&m_interface, SIGNAL(wallpaperChanged(QString)), this, SLOT(onWallpaperChanged(QString))); + connect(&m_interface, SIGNAL(darkModeDimsWallpaerChanged()), this, SIGNAL(dimsWallpaperChanged())); + + m_wallpaper = m_interface.property("wallpaper").toString(); + emit wallpaperChanged(); + } +} + +void DesktopSettings::onWallpaperChanged(QString path) +{ + if (path != m_wallpaper) { + m_wallpaper = path; + emit wallpaperChanged(); + } +} diff --git a/src/desktop/desktopsettings.h b/src/desktop/desktopsettings.h new file mode 100644 index 0000000..463dbe3 --- /dev/null +++ b/src/desktop/desktopsettings.h @@ -0,0 +1,34 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include +#include + +class DesktopSettings : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString wallpaper READ wallpaper NOTIFY wallpaperChanged) + Q_PROPERTY(bool dimsWallpaper READ dimsWallpaper NOTIFY dimsWallpaperChanged) + +public: + explicit DesktopSettings(QObject *parent = nullptr); + + QString wallpaper() const; + bool dimsWallpaper() const; + + Q_INVOKABLE void launch(const QString &command, const QStringList &args); + +signals: + void wallpaperChanged(); + void dimsWallpaperChanged(); + +private slots: + void init(); + void onWallpaperChanged(QString); + +private: + QDBusInterface m_interface; + QString m_wallpaper; +}; + +#endif // SETTINGS_H diff --git a/src/desktop/desktopview.cpp b/src/desktop/desktopview.cpp new file mode 100644 index 0000000..79221a4 --- /dev/null +++ b/src/desktop/desktopview.cpp @@ -0,0 +1,59 @@ +#include "desktopview.h" + +#include +#include + +#include +#include +#include + +#include + +DesktopView::DesktopView(QQuickView *parent) + : QQuickView(parent) +{ + m_screenRect = qApp->primaryScreen()->geometry(); + m_screenAvailableRect = qApp->primaryScreen()->availableGeometry(); + + // setFlags(Qt::Window | Qt::FramelessWindowHint); + setTitle(tr("Desktop")); + + KWindowSystem::setType(winId(), NET::Desktop); + KWindowSystem::setState(winId(), NET::KeepBelow); + + engine()->rootContext()->setContextProperty("desktopView", this); + + setScreen(qApp->primaryScreen()); + setResizeMode(QQuickView::SizeRootObjectToView); + setSource(QStringLiteral("qrc:/qml/Desktop/Desktop.qml")); + + onGeometryChanged(); + + connect(qApp->primaryScreen(), &QScreen::virtualGeometryChanged, this, &DesktopView::onGeometryChanged); + connect(qApp->primaryScreen(), &QScreen::geometryChanged, this, &DesktopView::onGeometryChanged); + connect(qApp->primaryScreen(), &QScreen::availableGeometryChanged, this, &DesktopView::onAvailableGeometryChanged); +} + +QRect DesktopView::screenRect() +{ + return m_screenRect; +} + +QRect DesktopView::screenAvailableRect() +{ + return m_screenAvailableRect; +} + +void DesktopView::onGeometryChanged() +{ + m_screenRect = qApp->primaryScreen()->geometry(); + setGeometry(qApp->primaryScreen()->geometry()); + + emit screenRectChanged(); +} + +void DesktopView::onAvailableGeometryChanged(const QRect &geometry) +{ + m_screenAvailableRect = geometry; + emit screenAvailableGeometryChanged(); +} diff --git a/src/desktop/desktopview.h b/src/desktop/desktopview.h new file mode 100644 index 0000000..f8c729c --- /dev/null +++ b/src/desktop/desktopview.h @@ -0,0 +1,31 @@ +#ifndef DESKTOPVIEW_H +#define DESKTOPVIEW_H + +#include + +class DesktopView : public QQuickView +{ + Q_OBJECT + Q_PROPERTY(QRect screenRect READ screenRect NOTIFY screenRectChanged) + Q_PROPERTY(QRect screenAvailableRect READ screenAvailableRect NOTIFY screenAvailableGeometryChanged) + +public: + explicit DesktopView(QQuickView *parent = nullptr); + + QRect screenRect(); + QRect screenAvailableRect(); + +signals: + void screenRectChanged(); + void screenAvailableGeometryChanged(); + +private slots: + void onGeometryChanged(); + void onAvailableGeometryChanged(const QRect &geometry); + +private: + QRect m_screenRect; + QRect m_screenAvailableRect; +}; + +#endif // DESKTOPVIEW_H diff --git a/src/dialogs/propertiesdialog.cpp b/src/dialogs/propertiesdialog.cpp new file mode 100644 index 0000000..2583932 --- /dev/null +++ b/src/dialogs/propertiesdialog.cpp @@ -0,0 +1,146 @@ +#include "propertiesdialog.h" +#include "../iconthemeprovider.h" + +#include +#include + +#include +#include +#include +#include + +PropertiesDialog::PropertiesDialog(const KFileItem &item, QObject *parent) + : QObject(parent) +{ + m_items.append(item); + init(); +} + +PropertiesDialog::PropertiesDialog(const KFileItemList &items, QObject *parent) + : QObject(parent) +{ + m_items = items; + init(); +} + +PropertiesDialog::PropertiesDialog(const QUrl &url, QObject *parent) + : QObject(parent) +{ + m_items.append(KFileItem(url)); + init(); +} + +PropertiesDialog::~PropertiesDialog() +{ + if (m_dirSizeJob) + m_dirSizeJob->kill(); +} + +void PropertiesDialog::showDialog(const KFileItem &item) +{ + PropertiesDialog *dlg = new PropertiesDialog(item); + QQmlApplicationEngine *engine = new QQmlApplicationEngine; + engine->addImageProvider(QStringLiteral("icontheme"), new IconThemeProvider()); + engine->rootContext()->setContextProperty("main", dlg); + engine->load(QUrl("qrc:/qml/Dialogs/PropertiesDialog.qml")); +} + +void PropertiesDialog::showDialog(const KFileItemList &items) +{ + PropertiesDialog *dlg = new PropertiesDialog(items); + QQmlApplicationEngine *engine = new QQmlApplicationEngine; + engine->addImageProvider(QStringLiteral("icontheme"), new IconThemeProvider()); + engine->rootContext()->setContextProperty("main", dlg); + engine->load(QUrl("qrc:/qml/Dialogs/PropertiesDialog.qml")); +} + +bool PropertiesDialog::multiple() const +{ + return m_multiple; +} + +QString PropertiesDialog::location() const +{ + return m_location; +} + +QString PropertiesDialog::fileName() const +{ + return m_fileName; +} + +QString PropertiesDialog::iconName() const +{ + return m_iconName; +} + +QString PropertiesDialog::mimeType() const +{ + return m_mimeType; +} + +QString PropertiesDialog::size() const +{ + return m_size; +} + +QString PropertiesDialog::creationTime() const +{ + return m_creationTime; +} + +QString PropertiesDialog::modifiedTime() const +{ + return m_modifiedTime; +} + +QString PropertiesDialog::accessedTime() const +{ + return m_accessedTime; +} + +void PropertiesDialog::init() +{ + m_multiple = m_items.count() > 1; + + m_dirSizeJob = KIO::directorySize(m_items); + + connect(m_dirSizeJob, &KIO::DirectorySizeJob::result, this, &PropertiesDialog::slotDirSizeFinished); + + if (!m_multiple) { + KFileItem item = m_items.first(); + + QString path; + m_fileName = m_items.first().name(); + + if (item.isDir()) + m_iconName = "folder"; + else + m_iconName = m_items.first().iconName(); + + m_mimeType = m_items.first().mimetype(); + m_size = KIO::convertSize(m_items.first().size()); + m_location = QFileInfo(m_items.first().localPath()).dir().path(); + + qDebug() << m_items.first().mimetype() << " ???"; + + m_creationTime = item.time(KFileItem::CreationTime).toString(); + m_modifiedTime = item.time(KFileItem::ModificationTime).toString(); + m_accessedTime = item.time(KFileItem::AccessTime).toString(); + } else { + m_fileName = QString("%1 files").arg(m_items.count()); + m_location = QFileInfo(m_items.first().localPath()).dir().path(); + } +} + +void PropertiesDialog::slotDirSizeFinished(KJob *job) +{ + if (job->error()) + return; + + m_size = KIO::convertSize(m_dirSizeJob->totalSize()); + + m_dirSizeJob = 0; + + emit sizeChanged(); +} diff --git a/src/dialogs/propertiesdialog.h b/src/dialogs/propertiesdialog.h new file mode 100644 index 0000000..95d0288 --- /dev/null +++ b/src/dialogs/propertiesdialog.h @@ -0,0 +1,71 @@ +#ifndef PROPERTIESDIALOG_H +#define PROPERTIESDIALOG_H + +#include +#include + +#include +#include + +class PropertiesDialog : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString location READ location CONSTANT) + Q_PROPERTY(QString fileName READ fileName NOTIFY fileNameChanged) + Q_PROPERTY(QString iconName READ iconName NOTIFY iconNameChanged) + Q_PROPERTY(QString mimeType READ mimeType CONSTANT) + Q_PROPERTY(QString size READ size NOTIFY sizeChanged) + Q_PROPERTY(QString creationTime READ creationTime CONSTANT) + Q_PROPERTY(QString modifiedTime READ modifiedTime CONSTANT) + Q_PROPERTY(QString accessedTime READ accessedTime CONSTANT) + Q_PROPERTY(bool multiple READ multiple CONSTANT) + +public: + explicit PropertiesDialog(const KFileItem &item, QObject *parent = nullptr); + explicit PropertiesDialog(const KFileItemList &items, QObject *parent = nullptr); + explicit PropertiesDialog(const QUrl &url, QObject *parent = nullptr); + ~PropertiesDialog(); + + static void showDialog(const KFileItem &item); + static void showDialog(const KFileItemList &items); + + bool multiple() const; + + QString location() const; + QString fileName() const; + QString iconName() const; + QString mimeType() const; + QString size() const; + + QString creationTime() const; + QString modifiedTime() const; + QString accessedTime() const; + +signals: + void fileNameChanged(); + void iconNameChanged(); + void sizeChanged(); + +private: + void init(); + +private slots: + void slotDirSizeFinished(KJob *job); + +private: + KFileItemList m_items; + QString m_location; + QString m_fileName; + QString m_iconName; + QString m_mimeType; + QString m_size; + QString m_creationTime; + QString m_modifiedTime; + QString m_accessedTime; + + KIO::DirectorySizeJob *m_dirSizeJob; + + bool m_multiple; +}; + +#endif // PROPERTIESDIALOG_H diff --git a/src/fm.cpp b/src/fm.cpp new file mode 100644 index 0000000..cef1801 --- /dev/null +++ b/src/fm.cpp @@ -0,0 +1,239 @@ +/* + * Copyright 2018 Camilo Higuita + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "fm.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +FM::FM(QObject *parent) + : QObject(parent) +#ifdef COMPONENT_SYNCING + , sync(new Syncing(this)) +#endif + , dirLister(new KCoreDirLister(this)) +{ + this->dirLister->setAutoUpdate(true); + + const static auto packItems = [](const KFileItemList &items) -> FMH::MODEL_LIST { + return std::accumulate(items.constBegin(), items.constEnd(), FMH::MODEL_LIST(), [](FMH::MODEL_LIST &res, const KFileItem &item) -> FMH::MODEL_LIST { + res << FMH::getFileInfo(item); + return res; + }); + }; + + connect(dirLister, static_cast(&KCoreDirLister::completed), this, [&](QUrl url) { + qDebug() << "PATH CONTENT READY" << url; + emit this->pathContentReady(url); + }); + + connect(dirLister, static_cast(&KCoreDirLister::itemsAdded), this, [&](QUrl dirUrl, KFileItemList items) { + qDebug() << "MORE ITEMS WERE ADDED"; + emit this->pathContentItemsReady({dirUrl, packItems(items)}); + }); + + // connect(dirLister, static_cast(&KCoreDirLister::newItems), [&](KFileItemList items) + // { + // qDebug()<< "MORE NEW ITEMS WERE ADDED"; + // for(const auto &item : items) + // qDebug()<< "MORE <<" << item.url(); + // + // emit this->pathContentChanged(dirLister->url()); + // }); + + connect(dirLister, static_cast(&KCoreDirLister::itemsDeleted), this, [&](KFileItemList items) { + qDebug() << "ITEMS WERE DELETED"; + emit this->pathContentItemsRemoved({dirLister->url(), packItems(items)}); + }); + + connect(dirLister, static_cast> &items)>(&KCoreDirLister::refreshItems), this, [&](QList> items) { + qDebug() << "ITEMS WERE REFRESHED"; + + const auto res = std::accumulate( + items.constBegin(), items.constEnd(), QVector>(), [](QVector> &list, const QPair &pair) -> QVector> { + list << QPair {FMH::getFileInfo(pair.first), FMH::getFileInfo(pair.second)}; + return list; + }); + + emit this->pathContentItemsChanged(res); + }); + +#ifdef COMPONENT_SYNCING + connect(this->sync, &Syncing::listReady, [this](const FMH::MODEL_LIST &list, const QUrl &url) { + emit this->cloudServerContentReady(list, url); + }); + + connect(this->sync, &Syncing::itemReady, [this](const FMH::MODEL &item, const QUrl &url, const Syncing::SIGNAL_TYPE &signalType) { + switch (signalType) { + case Syncing::SIGNAL_TYPE::OPEN: + FMStatic::openUrl(item[FMH::MODEL_KEY::PATH]); + break; + + case Syncing::SIGNAL_TYPE::DOWNLOAD: + emit this->cloudItemReady(item, url); + break; + + case Syncing::SIGNAL_TYPE::COPY: { + QVariantMap data; + const auto keys = item.keys(); + for (auto key : keys) + data.insert(FMH::MODEL_NAME[key], item[key]); + + // this->copy(QVariantList {data}, this->sync->getCopyTo()); + break; + } + default: + return; + } + }); + + connect(this->sync, &Syncing::error, [this](const QString &message) { + emit this->warningMessage(message); + }); + + connect(this->sync, &Syncing::progress, [this](const int &percent) { + emit this->loadProgress(percent); + }); + + connect(this->sync, &Syncing::dirCreated, [this](const FMH::MODEL &dir, const QUrl &url) { + emit this->newItem(dir, url); + }); + + connect(this->sync, &Syncing::uploadReady, [this](const FMH::MODEL &item, const QUrl &url) { + emit this->newItem(item, url); + }); +#endif +} + +void FM::getPathContent(const QUrl &path, const bool &hidden, const bool &onlyDirs, const QStringList &filters, const QDirIterator::IteratorFlags &iteratorFlags) +{ + qDebug() << "Getting async path contents"; + Q_UNUSED(iteratorFlags) + this->dirLister->setShowingDotFiles(hidden); + this->dirLister->setDirOnlyMode(onlyDirs); + this->dirLister->setNameFilter(filters.join(" ")); + + if (this->dirLister->openUrl(path)) + qDebug() << "GETTING PATH CONTENT" << path; +} + +FMH::MODEL_LIST FM::getAppsPath() +{ + return FMH::MODEL_LIST {FMH::MODEL {{FMH::MODEL_KEY::ICON, "system-run"}, + {FMH::MODEL_KEY::LABEL, FMH::PATHTYPE_LABEL[FMH::PATHTYPE_KEY::APPS_PATH]}, + {FMH::MODEL_KEY::PATH, FMH::PATHTYPE_URI[FMH::PATHTYPE_KEY::APPS_PATH]}, + {FMH::MODEL_KEY::TYPE, FMH::PATHTYPE_LABEL[FMH::PATHTYPE_KEY::PLACES_PATH]}}}; +} + +bool FM::getCloudServerContent(const QUrl &path, const QStringList &filters, const int &depth) +{ +#ifdef COMPONENT_SYNCING + const auto __list = path.toString().replace("cloud:///", "/").split("/"); + + if (__list.isEmpty() || __list.size() < 2) { + qWarning() << "Could not parse username to get cloud server content"; + return false; + } + + auto user = __list[1]; + // auto data = this->get(QString("select * from clouds where user = '%1'").arg(user)); + QVariantList data; + if (data.isEmpty()) + return false; + + auto map = data.first().toMap(); + + user = map[FMH::MODEL_NAME[FMH::MODEL_KEY::USER]].toString(); + auto server = map[FMH::MODEL_NAME[FMH::MODEL_KEY::SERVER]].toString(); + auto password = map[FMH::MODEL_NAME[FMH::MODEL_KEY::PASSWORD]].toString(); + this->sync->setCredentials(server, user, password); + + this->sync->listContent(path, filters, depth); + return true; +#else + Q_UNUSED(path) + Q_UNUSED(filters) + Q_UNUSED(depth) + + return false; +#endif +} + +void FM::createCloudDir(const QString &path, const QString &name) +{ +#ifdef COMPONENT_SYNCING + this->sync->createDir(path, name); +#endif +} + +void FM::openCloudItem(const QVariantMap &item) +{ +#ifdef COMPONENT_SYNCING + FMH::MODEL data; + const auto keys = item.keys(); + for (const auto &key : keys) + data.insert(FMH::MODEL_NAME_KEY[key], item[key].toString()); + + this->sync->resolveFile(data, Syncing::SIGNAL_TYPE::OPEN); +#endif +} + +void FM::getCloudItem(const QVariantMap &item) +{ +#ifdef COMPONENT_SYNCING + this->sync->resolveFile(FMH::toModel(item), Syncing::SIGNAL_TYPE::DOWNLOAD); +#endif +} + +QString FM::resolveUserCloudCachePath(const QString &server, const QString &user) +{ + Q_UNUSED(server) + return FMH::CloudCachePath + "opendesktop/" + user; +} + +QString FM::resolveLocalCloudPath(const QString &path) +{ +#ifdef COMPONENT_SYNCING + return QString(path).replace(FMH::PATHTYPE_URI[FMH::PATHTYPE_KEY::CLOUD_PATH] + this->sync->getUser(), ""); +#else + return QString(); +#endif +} + +bool FM::cut(const QList &urls, const QUrl &where) +{ + return FMStatic::cut(urls, where); +} + +bool FM::copy(const QList &urls, const QUrl &where) +{ + return FMStatic::copy(urls, where); +} diff --git a/src/fm.h b/src/fm.h new file mode 100644 index 0000000..97174b1 --- /dev/null +++ b/src/fm.h @@ -0,0 +1,206 @@ +#ifndef FM_H +#define FM_H + +#include +#include +#include +#include +#include +#include + +#include "fmh.h" +#include "fmstatic.h" + +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) +class KCoreDirLister; +#else +class QFileSystemWatcher; + +namespace FMH +{ +class FileLoader; +} +/** + * @brief The QDirLister class + * Placeholder for the KCoreDirLister for other system other than GNU Linux + */ +class QDirLister : public QObject +{ + Q_OBJECT +public: + explicit QDirLister(QObject *parent = nullptr); + +public slots: + /** + * @brief openUrl + * @param url + * @return + */ + bool openUrl(const QUrl &url); + + /** + * @brief setNameFilter + * @param filters + */ + void setNameFilter(QString filters); + + /** + * @brief setDirOnlyMode + * @param value + */ + void setDirOnlyMode(bool value); + + /** + * @brief setShowingDotFiles + * @param value + */ + void setShowingDotFiles(bool value); + +signals: + void itemsReady(FMH::MODEL_LIST items, QUrl url); + void itemReady(FMH::MODEL item, QUrl url); + void completed(QUrl url); + void itemsAdded(FMH::MODEL_LIST items, QUrl url); + void itemsDeleted(FMH::MODEL_LIST items, QUrl url); + void newItems(FMH::MODEL_LIST items, QUrl url); + void refreshItems(QVector> items, QUrl url); + +private: + FMH::FileLoader *m_loader; + QFileSystemWatcher *m_watcher; + + FMH::MODEL_LIST m_list; + QString m_nameFilters; + QUrl m_url; + bool m_dirOnly = false; + bool m_showDotFiles = false; + + bool m_checking = false; + + void reviewChanges(); + bool includes(const QUrl &url); + int indexOf(const FMH::MODEL_KEY &key, const QString &value) const; +}; +#endif + +class Syncing; +class Tagging; + +/** + * @brief The FM class + * File management methods with syncing and tagging integration if such components were enabled with COMPONENT_SYNCING and COMPONENT_TAGGING + */ +class FM : public QObject +{ + Q_OBJECT + +public: + FM(QObject *parent = nullptr); + + /** Syncing **/ + /** + * @brief getCloudServerContent + * Given a server URL address return the contents. This only works if the syncing component has been enabled COMPONENT_SYNCING + * @param server + * Server URL + * @param filters + * Filters to be applied + * @param depth + * How deep in the directory three go, for example 1 keeps the retrieval in the first level + * @return + */ + bool getCloudServerContent(const QUrl &server, const QStringList &filters = QStringList(), const int &depth = 0); + + /** + * @brief createCloudDir + * Creates a directory in the server. This only works if the syncing component has been enabled COMPONENT_SYNCING + * @param path + * Server address URL + * @param name + * Directory name + */ + Q_INVOKABLE void createCloudDir(const QString &path, const QString &name); + + /** + * @brief getPathContent + * Given a path URL extract the contents and return the information packaged as a model. This method is asyncronous and once items are ready signals are emitted, such as: pathContentItemsReady or pathContentReady + * @param path + * The directory path + * @param hidden + * If shoudl also pack hidden files + * @param onlyDirs + * Should only pack directories + * @param filters + * Filters to be applied to the retrieval + * @param iteratorFlags + * Directory iterator flags, for reference check QDirIterator documentation + */ + void getPathContent(const QUrl &path, const bool &hidden = false, const bool &onlyDirs = false, const QStringList &filters = QStringList(), const QDirIterator::IteratorFlags &iteratorFlags = QDirIterator::NoIteratorFlags); + + /** + * @brief resolveLocalCloudPath + * Given a server address URL resolve it to the local cache URL. This only works if the syncing component has been enabled COMPONENT_SYNCING + * @param path + * Server address + * @return + */ + QString resolveLocalCloudPath(const QString &path); + + /** + * @brief getAppsPath + * Gives the path to the applications directory. Missing integration with other system other than GNU Linux + * @return + */ + static FMH::MODEL_LIST getAppsPath(); + + /** + * @brief resolveUserCloudCachePath + * @param server + * @param user + * @return + */ + static QString resolveUserCloudCachePath(const QString &server, const QString &user); + +#ifdef COMPONENT_SYNCING + Syncing *sync; +#endif + +private: +#ifdef COMPONENT_TAGGING + Tagging *tag; +#endif + +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + KCoreDirLister *dirLister; +#else + QDirLister *dirLister; +#endif + +signals: + void cloudServerContentReady(FMH::MODEL_LIST list, const QUrl &url); + void cloudItemReady(FMH::MODEL item, QUrl path); // when a item is downloaded and ready + + void pathContentReady(QUrl path); + void pathContentItemsReady(FMH::PATH_CONTENT list); + void pathContentChanged(QUrl path); + void pathContentItemsChanged(QVector> items); + void pathContentItemsRemoved(FMH::PATH_CONTENT list); + + void warningMessage(QString message); + void loadProgress(int percent); + + void dirCreated(FMH::MODEL dir); + void newItem(FMH::MODEL item, QUrl path); // when a new item is created + +public slots: + void openCloudItem(const QVariantMap &item); + void getCloudItem(const QVariantMap &item); + + /* ACTIONS */ + bool copy(const QList &urls, const QUrl &where); + bool cut(const QList &urls, const QUrl &where); + + friend class FMStatic; +}; + +#endif // FM_H diff --git a/src/fmh.cpp b/src/fmh.cpp new file mode 100644 index 0000000..d53c80a --- /dev/null +++ b/src/fmh.cpp @@ -0,0 +1,337 @@ +#include "fmh.h" + +namespace FMH +{ +const QVector modelRoles(const FMH::MODEL &model) +{ + const auto keys = model.keys(); + return std::accumulate(keys.begin(), keys.end(), QVector(), [](QVector &res, const FMH::MODEL_KEY &key) { + res.append(key); + return res; + }); +} + +const QString mapValue(const QVariantMap &map, const FMH::MODEL_KEY &key) +{ + return map[FMH::MODEL_NAME[key]].toString(); +} + +const QVariantMap toMap(const FMH::MODEL &model) +{ + QVariantMap map; + for (const auto &key : model.keys()) + map.insert(FMH::MODEL_NAME[key], model[key]); + + return map; +} + +const FMH::MODEL toModel(const QVariantMap &map) +{ + FMH::MODEL model; + for (const auto &key : map.keys()) + model.insert(FMH::MODEL_NAME_KEY[key], map[key].toString()); + + return model; +} + +const FMH::MODEL_LIST toModelList(const QVariantList &list) +{ + FMH::MODEL_LIST res; + return std::accumulate(list.constBegin(), list.constEnd(), res, [](FMH::MODEL_LIST &res, const QVariant &item) -> FMH::MODEL_LIST { + res << FMH::toModel(item.toMap()); + return res; + }); +} + +const QVariantList toMapList(const FMH::MODEL_LIST &list) +{ + QVariantList res; + return std::accumulate(list.constBegin(), list.constEnd(), res, [](QVariantList &res, const FMH::MODEL &item) -> QVariantList { + res << FMH::toMap(item); + return res; + }); +} + +const FMH::MODEL filterModel(const FMH::MODEL &model, const QVector &keys) +{ + FMH::MODEL res; + return std::accumulate(keys.constBegin(), keys.constEnd(), res, [=](FMH::MODEL &res, const FMH::MODEL_KEY &key) -> FMH::MODEL { + if (model.contains(key)) + res[key] = model[key]; + return res; + }); +} + +const QStringList modelToList(const FMH::MODEL_LIST &list, const FMH::MODEL_KEY &key) +{ + QStringList res; + return std::accumulate(list.constBegin(), list.constEnd(), res, [key](QStringList &res, const FMH::MODEL &item) -> QStringList { + if (item.contains(key)) + res << item[key]; + return res; + }); +} + +bool fileExists(const QUrl &path) +{ + if (!path.isLocalFile()) { + qWarning() << "URL recived is not a local file" << path; + return false; + } + return QFileInfo::exists(path.toLocalFile()); +} + +const QString fileDir(const QUrl &path) // the directory path of the file +{ + QString res = path.toString(); + if (path.isLocalFile()) { + const QFileInfo file(path.toLocalFile()); + if (file.isDir()) + res = path.toString(); + else + res = QUrl::fromLocalFile(file.dir().absolutePath()).toString(); + } else + qWarning() << "The path is not a local one. FM::fileDir"; + + return res; +} + +const QUrl parentDir(const QUrl &path) +{ + if (!path.isLocalFile()) { + qWarning() << "URL recived is not a local file, FM::parentDir" << path; + return path; + } + + QDir dir(path.toLocalFile()); + dir.cdUp(); + return QUrl::fromLocalFile(dir.absolutePath()); +} + +const QVariantMap dirConf(const QUrl &path) +{ + if (!path.isLocalFile()) { + qWarning() << "URL recived is not a local file" << path; + return QVariantMap(); + } + + if (!fileExists(path)) + return QVariantMap(); + + QString icon, iconsize, hidden, detailview, showthumbnail, showterminal; + + uint count = 0, sortby = MODEL_KEY::MODIFIED, viewType = 0; + + bool foldersFirst = false; + +#if defined Q_OS_ANDROID || defined Q_OS_WIN || defined Q_OS_MACOS || defined Q_OS_IOS + QSettings file(path.toLocalFile(), QSettings::Format::NativeFormat); + file.beginGroup(QString("Desktop Entry")); + icon = file.value("Icon").toString(); + file.endGroup(); + + file.beginGroup(QString("Settings")); + hidden = file.value("HiddenFilesShown").toString(); + file.endGroup(); + + file.beginGroup(QString("MAUIFM")); + iconsize = file.value("IconSize").toString(); + detailview = file.value("DetailView").toString(); + showthumbnail = file.value("ShowThumbnail").toString(); + showterminal = file.value("ShowTerminal").toString(); + count = file.value("Count").toInt(); + sortby = file.value("SortBy").toInt(); + foldersFirst = file.value("FoldersFirst").toBool(); + viewType = file.value("ViewType").toInt(); + file.endGroup(); + +#else + KConfig file(path.toLocalFile()); + icon = file.entryMap(QString("Desktop Entry"))["Icon"]; + hidden = file.entryMap(QString("Settings"))["HiddenFilesShown"]; + iconsize = file.entryMap(QString("MAUIFM"))["IconSize"]; + detailview = file.entryMap(QString("MAUIFM"))["DetailView"]; + showthumbnail = file.entryMap(QString("MAUIFM"))["ShowThumbnail"]; + showterminal = file.entryMap(QString("MAUIFM"))["ShowTerminal"]; + count = file.entryMap(QString("MAUIFM"))["Count"].toInt(); + sortby = file.entryMap(QString("MAUIFM"))["SortBy"].toInt(); + foldersFirst = file.entryMap(QString("MAUIFM"))["FoldersFirst"] == "true" ? true : false; + viewType = file.entryMap(QString("MAUIFM"))["ViewType"].toInt(); +#endif + + return QVariantMap({{MODEL_NAME[MODEL_KEY::ICON], icon.isEmpty() ? "folder" : icon}, + {MODEL_NAME[MODEL_KEY::ICONSIZE], iconsize}, + {MODEL_NAME[MODEL_KEY::COUNT], count}, + {MODEL_NAME[MODEL_KEY::SHOWTERMINAL], showterminal.isEmpty() ? "false" : showterminal}, + {MODEL_NAME[MODEL_KEY::SHOWTHUMBNAIL], showthumbnail.isEmpty() ? "false" : showthumbnail}, + {MODEL_NAME[MODEL_KEY::DETAILVIEW], detailview.isEmpty() ? "false" : detailview}, + {MODEL_NAME[MODEL_KEY::HIDDEN], hidden.isEmpty() ? false : (hidden == "true" ? true : false)}, + {MODEL_NAME[MODEL_KEY::SORTBY], sortby}, + {MODEL_NAME[MODEL_KEY::FOLDERSFIRST], foldersFirst}, + {MODEL_NAME[MODEL_KEY::VIEWTYPE], viewType}}); +} + +void setDirConf(const QUrl &path, const QString &group, const QString &key, const QVariant &value) +{ + if (!path.isLocalFile()) { + qWarning() << "URL recived is not a local file" << path; + return; + } + +#if defined Q_OS_ANDROID || defined Q_OS_WIN || defined Q_OS_MACOS || defined Q_OS_IOS + QSettings file(path.toLocalFile(), QSettings::Format::IniFormat); + file.beginGroup(group); + file.setValue(key, value); + file.endGroup(); + file.sync(); +#else + KConfig file(path.toLocalFile(), KConfig::SimpleConfig); + auto kgroup = file.group(group); + kgroup.writeEntry(key, value); + // file.reparseConfiguration(); + file.sync(); +#endif +} + +const QString getIconName(const QUrl &path) +{ + if (path.isLocalFile() && QFileInfo(path.toLocalFile()).isDir()) { + if (folderIcon.contains(path.toString())) + return folderIcon[path.toString()]; + else { + const auto icon = dirConf(QString(path.toString() + "/%1").arg(".directory"))[MODEL_NAME[MODEL_KEY::ICON]].toString(); + return icon.isEmpty() ? "folder" : icon; + } + + } else { + QMimeDatabase mime; + const auto type = mime.mimeTypeForFile(path.toString()); + return type.iconName(); + +// KFileItem mime(path); +// return mime.iconName(); + } +} + +const QString getMime(const QUrl &path) +{ + if (!path.isLocalFile()) { + qWarning() << "URL recived is not a local file, getMime" << path; + return QString(); + } + + const QMimeDatabase mimedb; + return mimedb.mimeTypeForFile(path.toLocalFile()).name(); +} + +const QUrl thumbnailUrl(const QUrl &url, const QString &mimetype) +{ +#if defined Q_OS_LINUX && !defined Q_OS_ANDROID + if (checkFileType(FILTER_TYPE::DOCUMENT, mimetype) || checkFileType(FILTER_TYPE::VIDEO, mimetype)) { + return QUrl("image://thumbnailer/" + url.toString()); + } +#endif + + if (checkFileType(FILTER_TYPE::IMAGE, mimetype)) { + return url; + } + + return QUrl(); +} + +#if (!defined Q_OS_ANDROID && defined Q_OS_LINUX) || defined Q_OS_WIN +const FMH::MODEL getFileInfo(const KFileItem &kfile) +{ + return MODEL {{MODEL_KEY::LABEL, kfile.name()}, + {MODEL_KEY::NAME, kfile.name().remove(kfile.name().lastIndexOf("."), kfile.name().size())}, + {MODEL_KEY::DATE, kfile.time(KFileItem::FileTimes::CreationTime).toString(Qt::TextDate)}, + {MODEL_KEY::MODIFIED, kfile.time(KFileItem::FileTimes::ModificationTime).toString(Qt::TextDate)}, + {MODEL_KEY::LAST_READ, kfile.time(KFileItem::FileTimes::AccessTime).toString(Qt::TextDate)}, + {MODEL_KEY::PATH, kfile.mostLocalUrl().toString()}, + {MODEL_KEY::URL, kfile.mostLocalUrl().toString()}, + {MODEL_KEY::THUMBNAIL, thumbnailUrl(kfile.mostLocalUrl(), kfile.mimetype()).toString()}, + {MODEL_KEY::SYMLINK, kfile.linkDest()}, + {MODEL_KEY::IS_SYMLINK, QVariant(kfile.isLink()).toString()}, + {MODEL_KEY::HIDDEN, QVariant(kfile.isHidden()).toString()}, + {MODEL_KEY::IS_DIR, QVariant(kfile.isDir()).toString()}, + {MODEL_KEY::IS_FILE, QVariant(kfile.isFile()).toString()}, + {MODEL_KEY::WRITABLE, QVariant(kfile.isWritable()).toString()}, + {MODEL_KEY::READABLE, QVariant(kfile.isReadable()).toString()}, + {MODEL_KEY::EXECUTABLE, QVariant(kfile.isDesktopFile()).toString()}, + {MODEL_KEY::MIME, kfile.mimetype()}, + {MODEL_KEY::GROUP, kfile.group()}, + {MODEL_KEY::ICON, kfile.iconName()}, + // for set wallpaper. + {MODEL_KEY::IMG, QVariant(kfile.mimetype().startsWith("image/")).toString()}, + {MODEL_KEY::SIZE, QString::number(kfile.size())}, + {MODEL_KEY::OWNER, kfile.user()}, + {MODEL_KEY::COUNT, kfile.isLocalFile() && kfile.isDir() ? QString::number(QDir(kfile.localPath()).count()) : "0"}}; +} +#endif + +const FMH::MODEL getFileInfoModel(const QUrl &path) +{ + MODEL res; +#if defined Q_OS_ANDROID || defined Q_OS_MACOS || defined Q_OS_IOS || defined Q_OS_WIN + const QFileInfo file(path.toLocalFile()); + if (!file.exists()) + return MODEL(); + + const auto mime = getMime(path); + res = MODEL {{MODEL_KEY::GROUP, file.group()}, + {MODEL_KEY::OWNER, file.owner()}, + {MODEL_KEY::SUFFIX, file.completeSuffix()}, + {MODEL_KEY::LABEL, /*file.isDir() ? file.baseName() :*/ path == HomePath ? QStringLiteral("Home") : file.fileName()}, + {MODEL_KEY::NAME, file.fileName()}, + {MODEL_KEY::DATE, file.birthTime().toString(Qt::TextDate)}, + {MODEL_KEY::MODIFIED, file.lastModified().toString(Qt::TextDate)}, + {MODEL_KEY::LAST_READ, file.lastRead().toString(Qt::TextDate)}, + {MODEL_KEY::MIME, mime}, + {MODEL_KEY::SYMLINK, file.symLinkTarget()}, + {MODEL_KEY::IS_SYMLINK, QVariant(file.isSymLink()).toString()}, + {MODEL_KEY::IS_FILE, QVariant(file.isFile()).toString()}, + {MODEL_KEY::HIDDEN, QVariant(file.isHidden()).toString()}, + {MODEL_KEY::IS_DIR, QVariant(file.isDir()).toString()}, + {MODEL_KEY::WRITABLE, QVariant(file.isWritable()).toString()}, + {MODEL_KEY::READABLE, QVariant(file.isReadable()).toString()}, + {MODEL_KEY::EXECUTABLE, QVariant(file.suffix().endsWith(".desktop")).toString()}, + {MODEL_KEY::ICON, getIconName(path)}, + {MODEL_KEY::SIZE, QString::number(file.size()) /*locale.formattedDataSize(file.size())*/}, + {MODEL_KEY::PATH, path.toString()}, + {MODEL_KEY::URL, path.toString()}, + {MODEL_KEY::THUMBNAIL, thumbnailUrl(path, mime).toString()}, + {MODEL_KEY::COUNT, file.isDir() ? QString::number(QDir(path.toLocalFile()).count()) : "0"}}; +#else + res = getFileInfo(KFileItem(path, KFileItem::MimeTypeDetermination::NormalMimeTypeDetermination)); +#endif + return res; +} + +const QVariantMap getFileInfo(const QUrl &path) +{ + return toMap(getFileInfoModel(path)); +} + +const MODEL getDirInfoModel(const QUrl &path, const QString &type) +{ + auto res = getFileInfoModel(path); + res[MODEL_KEY::TYPE] = type; + return res; +} + +const QVariantMap getDirInfo(const QUrl &path) +{ + return toMap(getDirInfoModel(path)); +} + +PATHTYPE_KEY getPathType(const QUrl &url) +{ + return PATHTYPE_SCHEME_NAME[url.scheme()]; +} + +bool checkFileType(const FMH::FILTER_TYPE &type, const QString &mimeTypeName) +{ + return SUPPORTED_MIMETYPES[type].contains(mimeTypeName); +} + +} diff --git a/src/fmh.h b/src/fmh.h new file mode 100644 index 0000000..91229f3 --- /dev/null +++ b/src/fmh.h @@ -0,0 +1,1019 @@ +/* + * Copyright 2018 Camilo Higuita + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef FMH_H +#define FMH_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(Q_OS_ANDROID) +#include "mauiandroid.h" +#elif defined Q_OS_LINUX || defined Q_OS_WIN +#include +#include +#include +#include +#endif + +/** + * A set of helpers related to file management and modeling of data + */ +namespace FMH +{ +/** + * @brief isAndroid + * @return + */ +bool isAndroid(); + +/** + * @brief isWindows + * @return + */ +bool isWindows(); + +/** + * @brief isLinux + * @return + */ +bool isLinux(); + +/** + * @brief isMac + * @return + */ +bool isMac(); + +/** + * @brief isIOS + * @return + */ +bool isIOS(); + +/** + * @brief The FILTER_TYPE enum + */ +enum FILTER_TYPE : int { AUDIO, VIDEO, TEXT, IMAGE, DOCUMENT, COMPRESSED, FONT, NONE }; + +static const QStringList AUDIO_MIMETYPES = {"audio/mpeg", "audio/mp4", "audio/flac", "audio/ogg", "audio/wav"}; + +static const QStringList VIDEO_MIMETYPES = {"video/mp4", "video/x-matroska", "video/webm", "video/avi", "video/flv", "video/mpg", "video/wmv", "video/mov", "video/ogg", "video/mpeg", "video/jpeg"}; + +static const QStringList TEXT_MIMETYPES = {"text/markdown", + "text/x-chdr", + "text/x-c++src", + "text/x-c++hdr", + "text/css", + "text/html", + "text/plain", + "text/richtext", + "text/scriptlet", + "text/x-vcard", + "text/x-go", + "text/x-cmake", + "text/x-qml", + "application/xml", + "application/javascript", + "application/json", + "application/pgp-keys", + "application/x-shellscript", + "application/x-cmakecache", + "application/x-kicad-project"}; + +static const QStringList IMAGE_MIMETYPES = {"image/bmp", "image/webp", "image/png", "image/gif", "image/jpeg", "image/web", "image/svg", "image/svg+xml"}; + +static const QStringList DOCUMENT_MIMETYPES = {"application/pdf", "application/rtf", "application/doc", "application/odf"}; + +static const QStringList COMPRESSED_MIMETYPES = + {"application/x-compress", "application/x-compressed", "application/x-xz-compressed-tar", "application/x-compressed-tar", "application/x-xz", "application/x-bzip", "application/x-gtar", "application/x-gzip", "application/zip"}; + +static const QStringList FONT_MIMETYPES = {"font/ttf", "font/otf"}; + +static const QMap SUPPORTED_MIMETYPES {{FILTER_TYPE::AUDIO, AUDIO_MIMETYPES}, + {FILTER_TYPE::VIDEO, VIDEO_MIMETYPES}, + {FILTER_TYPE::TEXT, TEXT_MIMETYPES}, + {FILTER_TYPE::IMAGE, IMAGE_MIMETYPES}, + {FILTER_TYPE::DOCUMENT, DOCUMENT_MIMETYPES}, + {FILTER_TYPE::FONT, FONT_MIMETYPES}, + {FILTER_TYPE::COMPRESSED, COMPRESSED_MIMETYPES}}; + +/** + * @brief getMimeTypeSuffixes + * @param type + * @return + */ +static QStringList getMimeTypeSuffixes(const FMH::FILTER_TYPE &type, QString (*cb)(QString)) +{ + QStringList res; + QMimeDatabase mimedb; + for (const auto &mime : SUPPORTED_MIMETYPES[type]) { + if (cb) { + const auto suffixes = mimedb.mimeTypeForName(mime).suffixes(); + for (const QString &_suffix : suffixes) { + res << cb(_suffix); + } + } else { + res << mimedb.mimeTypeForName(mime).suffixes(); + } + } + return res; +} + +static QHash FILTER_LIST = {{FILTER_TYPE::AUDIO, + getMimeTypeSuffixes(FILTER_TYPE::AUDIO, + [](QString suffix) -> QString { + return "*." + suffix; + })}, + {FILTER_TYPE::VIDEO, + getMimeTypeSuffixes(FILTER_TYPE::VIDEO, + [](QString suffix) -> QString { + return "*." + suffix; + })}, + {FILTER_TYPE::TEXT, + getMimeTypeSuffixes(FILTER_TYPE::TEXT, + [](QString suffix) -> QString { + return "*." + suffix; + })}, + {FILTER_TYPE::DOCUMENT, + getMimeTypeSuffixes(FILTER_TYPE::DOCUMENT, + [](QString suffix) -> QString { + return "*." + suffix; + })}, + {FILTER_TYPE::COMPRESSED, + getMimeTypeSuffixes(FILTER_TYPE::COMPRESSED, + [](QString suffix) -> QString { + return "*." + suffix; + })}, + {FILTER_TYPE::FONT, + getMimeTypeSuffixes(FILTER_TYPE::FONT, + [](QString suffix) -> QString { + return "*." + suffix; + })}, + {FILTER_TYPE::IMAGE, + getMimeTypeSuffixes(FILTER_TYPE::IMAGE, + [](QString suffix) -> QString { + return "*." + suffix; + })}, + {FILTER_TYPE::NONE, QStringList()}}; + +/** + * @brief The MODEL_KEY enum + */ +enum MODEL_KEY : int { + ICON, + LABEL, + PATH, + URL, + TYPE, + GROUP, + OWNER, + SUFFIX, + NAME, + DATE, + SIZE, + MODIFIED, + MIME, + TAG, + PERMISSIONS, + THUMBNAIL, + THUMBNAIL_1, + THUMBNAIL_2, + THUMBNAIL_3, + HIDDEN, + ICONSIZE, + DETAILVIEW, + SHOWTHUMBNAIL, + SHOWTERMINAL, + COUNT, + SORTBY, + USER, + PASSWORD, + SERVER, + FOLDERSFIRST, + VIEWTYPE, + ADDDATE, + FAV, + FAVORITE, + COLOR, + RATE, + FORMAT, + PLACE, + LOCATION, + ALBUM, + ARTIST, + TRACK, + DURATION, + ARTWORK, + PLAYLIST, + LYRICS, + WIKI, + MOOD, + SOURCETYPE, + GENRE, + NOTE, + COMMENT, + CONTEXT, + SOURCE, + TITLE, + ID, + PARENT_ID, + RELEASEDATE, + LICENSE, + DESCRIPTION, + BOOKMARK, + ACCOUNT, + ACCOUNTTYPE, + VERSION, + DOMAIN_M, + CATEGORY, + CONTENT, + PIN, + IMG, + PREVIEW, + LINK, + STAMP, + BOOK, + + /** ccdav keys **/ + N, + PHOTO, + GENDER, + ADR, + ADR_2, + ADR_3, + EMAIL, + EMAIL_2, + EMAIL_3, + LANG, + NICKNAME, + ORG, + PROFILE, + TZ, + TEL, + TEL_2, + TEL_3, + IM, + + /** other keys **/ + CITY, + STATE, + COUNTRY, + + /** keys from opendesktop store **/ + PACKAGE_ARCH, + PACKAGE_TYPE, + GPG_FINGERPRINT, + GPG_SIGNATURE, + PACKAGE_NAME, + PRICE, + REPOSITORY, + TAGS, + WAY, + PIC, + SMALL_PIC, + CHANGED, + COMMENTS, + CREATED, + DETAIL_PAGE, + DETAILS, + TOTAL_DOWNLOADS, + GHNS_EXCLUDED, + LANGUAGE, + PERSON_ID, + SCORE, + SUMMARY, + TYPE_ID, + TYPE_NAME, + XDG_TYPE, + + // file props + SYMLINK, + IS_SYMLINK, + IS_DIR, + IS_FILE, + IS_REMOTE, + EXECUTABLE, + READABLE, + WRITABLE, + LAST_READ, + VALUE, + KEY, + + MAC, + LOT, + APP, + URI, + DEVICE, + LASTSYNC + +}; + +static const QHash MODEL_NAME = {{MODEL_KEY::ICON, "icon"}, + {MODEL_KEY::LABEL, "label"}, + {MODEL_KEY::PATH, "path"}, + {MODEL_KEY::URL, "url"}, + {MODEL_KEY::TYPE, "type"}, + {MODEL_KEY::GROUP, "group"}, + {MODEL_KEY::OWNER, "owner"}, + {MODEL_KEY::SUFFIX, "suffix"}, + {MODEL_KEY::NAME, "name"}, + {MODEL_KEY::DATE, "date"}, + {MODEL_KEY::MODIFIED, "modified"}, + {MODEL_KEY::MIME, "mime"}, + {MODEL_KEY::SIZE, "size"}, + {MODEL_KEY::TAG, "tag"}, + {MODEL_KEY::PERMISSIONS, "permissions"}, + {MODEL_KEY::THUMBNAIL, "thumbnail"}, + {MODEL_KEY::THUMBNAIL_1, "thumbnail_1"}, + {MODEL_KEY::THUMBNAIL_2, "thumbnail_2"}, + {MODEL_KEY::THUMBNAIL_3, "thumbnail_3"}, + {MODEL_KEY::ICONSIZE, "iconsize"}, + {MODEL_KEY::HIDDEN, "hidden"}, + {MODEL_KEY::DETAILVIEW, "detailview"}, + {MODEL_KEY::SHOWTERMINAL, "showterminal"}, + {MODEL_KEY::SHOWTHUMBNAIL, "showthumbnail"}, + {MODEL_KEY::COUNT, "count"}, + {MODEL_KEY::SORTBY, "sortby"}, + {MODEL_KEY::USER, "user"}, + {MODEL_KEY::PASSWORD, "password"}, + {MODEL_KEY::SERVER, "server"}, + {MODEL_KEY::FOLDERSFIRST, "foldersfirst"}, + {MODEL_KEY::VIEWTYPE, "viewtype"}, + {MODEL_KEY::ADDDATE, "adddate"}, + {MODEL_KEY::FAV, "fav"}, + {MODEL_KEY::FAVORITE, "favorite"}, + {MODEL_KEY::COLOR, "color"}, + {MODEL_KEY::RATE, "rate"}, + {MODEL_KEY::FORMAT, "format"}, + {MODEL_KEY::PLACE, "place"}, + {MODEL_KEY::LOCATION, "location"}, + {MODEL_KEY::ALBUM, "album"}, + {MODEL_KEY::DURATION, "duration"}, + {MODEL_KEY::RELEASEDATE, "releasedate"}, + {MODEL_KEY::ARTIST, "artist"}, + {MODEL_KEY::LYRICS, "lyrics"}, + {MODEL_KEY::TRACK, "track"}, + {MODEL_KEY::GENRE, "genre"}, + {MODEL_KEY::WIKI, "wiki"}, + {MODEL_KEY::CONTEXT, "context"}, + {MODEL_KEY::SOURCETYPE, "sourcetype"}, + {MODEL_KEY::ARTWORK, "artwork"}, + {MODEL_KEY::NOTE, "note"}, + {MODEL_KEY::MOOD, "mood"}, + {MODEL_KEY::COMMENT, "comment"}, + {MODEL_KEY::PLAYLIST, "playlist"}, + {MODEL_KEY::SOURCE, "source"}, + {MODEL_KEY::TITLE, "title"}, + {MODEL_KEY::ID, "id"}, + {MODEL_KEY::PERSON_ID, "personid"}, + {MODEL_KEY::PARENT_ID, "parentid"}, + {MODEL_KEY::LICENSE, "license"}, + {MODEL_KEY::DESCRIPTION, "description"}, + {MODEL_KEY::BOOKMARK, "bookmark"}, + {MODEL_KEY::ACCOUNT, "account"}, + {MODEL_KEY::ACCOUNTTYPE, "accounttype"}, + {MODEL_KEY::VERSION, "version"}, + {MODEL_KEY::DOMAIN_M, "domain"}, + {MODEL_KEY::CATEGORY, "category"}, + {MODEL_KEY::CONTENT, "content"}, + {MODEL_KEY::PIN, "pin"}, + {MODEL_KEY::IMG, "img"}, + {MODEL_KEY::PREVIEW, "preview"}, + {MODEL_KEY::LINK, "link"}, + {MODEL_KEY::STAMP, "stamp"}, + {MODEL_KEY::BOOK, "book"}, + + /** ccdav keys **/ + {MODEL_KEY::N, "n"}, + {MODEL_KEY::IM, "im"}, + {MODEL_KEY::PHOTO, "photo"}, + {MODEL_KEY::GENDER, "gender"}, + {MODEL_KEY::ADR, "adr"}, + {MODEL_KEY::ADR_2, "adr2"}, + {MODEL_KEY::ADR_3, "adr3"}, + {MODEL_KEY::EMAIL, "email"}, + {MODEL_KEY::EMAIL_2, "email2"}, + {MODEL_KEY::EMAIL_3, "email3"}, + {MODEL_KEY::LANG, "lang"}, + {MODEL_KEY::NICKNAME, "nickname"}, + {MODEL_KEY::ORG, "org"}, + {MODEL_KEY::PROFILE, "profile"}, + {MODEL_KEY::TZ, "tz"}, + {MODEL_KEY::TEL, "tel"}, + {MODEL_KEY::TEL_2, "tel2"}, + {MODEL_KEY::TEL_3, "tel3"}, + + {MODEL_KEY::CITY, "city"}, + {MODEL_KEY::STATE, "state"}, + {MODEL_KEY::COUNTRY, "country"}, + + // opendesktop keys + {MODEL_KEY::PACKAGE_ARCH, "packagearch"}, + {MODEL_KEY::PACKAGE_TYPE, "packagetype"}, + {MODEL_KEY::GPG_FINGERPRINT, "gpgfingerprint"}, + {MODEL_KEY::GPG_SIGNATURE, "gpgsignature"}, + {MODEL_KEY::PACKAGE_NAME, "packagename"}, + {MODEL_KEY::PRICE, "price"}, + {MODEL_KEY::REPOSITORY, "repository"}, + {MODEL_KEY::TAGS, "tags"}, + {MODEL_KEY::WAY, "way"}, + {MODEL_KEY::PIC, "pic"}, + {MODEL_KEY::SMALL_PIC, "smallpic"}, + {MODEL_KEY::CHANGED, "changed"}, + {MODEL_KEY::COMMENTS, "comments"}, + {MODEL_KEY::CREATED, "created"}, + {MODEL_KEY::DETAIL_PAGE, "detailpage"}, + {MODEL_KEY::DETAILS, "details"}, + {MODEL_KEY::TOTAL_DOWNLOADS, "totaldownloads"}, + {MODEL_KEY::GHNS_EXCLUDED, "ghnsexcluded"}, + {MODEL_KEY::LANGUAGE, "language"}, + {MODEL_KEY::SCORE, "score"}, + {MODEL_KEY::SUMMARY, "summary"}, + {MODEL_KEY::TYPE_ID, "typeid"}, + {MODEL_KEY::TYPE_NAME, "typename"}, + {MODEL_KEY::XDG_TYPE, "xdgtype"}, + + // file props + {MODEL_KEY::SYMLINK, "symlink"}, + {MODEL_KEY::IS_SYMLINK, "issymlink"}, + {MODEL_KEY::LAST_READ, "lastread"}, + {MODEL_KEY::READABLE, "readable"}, + {MODEL_KEY::WRITABLE, "writeable"}, + {MODEL_KEY::IS_DIR, "isdir"}, + {MODEL_KEY::IS_FILE, "isfile"}, + {MODEL_KEY::IS_REMOTE, "isremote"}, + {MODEL_KEY::EXECUTABLE, "executable"}, + {MODEL_KEY::VALUE, "value"}, + {MODEL_KEY::KEY, "key"}, + + {MODEL_KEY::MAC, "mac"}, + {MODEL_KEY::LOT, "lot"}, + {MODEL_KEY::APP, "app"}, + {MODEL_KEY::URI, "uri"}, + {MODEL_KEY::DEVICE, "device"}, + {MODEL_KEY::LASTSYNC, "lastsync"}}; + +static const QHash MODEL_NAME_KEY = {{MODEL_NAME[MODEL_KEY::ICON], MODEL_KEY::ICON}, + {MODEL_NAME[MODEL_KEY::LABEL], MODEL_KEY::LABEL}, + {MODEL_NAME[MODEL_KEY::PATH], MODEL_KEY::PATH}, + {MODEL_NAME[MODEL_KEY::URL], MODEL_KEY::URL}, + {MODEL_NAME[MODEL_KEY::TYPE], MODEL_KEY::TYPE}, + {MODEL_NAME[MODEL_KEY::GROUP], MODEL_KEY::GROUP}, + {MODEL_NAME[MODEL_KEY::OWNER], MODEL_KEY::OWNER}, + {MODEL_NAME[MODEL_KEY::SUFFIX], MODEL_KEY::SUFFIX}, + {MODEL_NAME[MODEL_KEY::NAME], MODEL_KEY::NAME}, + {MODEL_NAME[MODEL_KEY::DATE], MODEL_KEY::DATE}, + {MODEL_NAME[MODEL_KEY::MODIFIED], MODEL_KEY::MODIFIED}, + {MODEL_NAME[MODEL_KEY::MIME], MODEL_KEY::MIME}, + { + MODEL_NAME[MODEL_KEY::SIZE], + MODEL_KEY::SIZE, + }, + {MODEL_NAME[MODEL_KEY::TAG], MODEL_KEY::TAG}, + {MODEL_NAME[MODEL_KEY::PERMISSIONS], MODEL_KEY::PERMISSIONS}, + {MODEL_NAME[MODEL_KEY::THUMBNAIL], MODEL_KEY::THUMBNAIL}, + {MODEL_NAME[MODEL_KEY::THUMBNAIL_1], MODEL_KEY::THUMBNAIL_1}, + {MODEL_NAME[MODEL_KEY::THUMBNAIL_2], MODEL_KEY::THUMBNAIL_2}, + {MODEL_NAME[MODEL_KEY::THUMBNAIL_3], MODEL_KEY::THUMBNAIL_3}, + {MODEL_NAME[MODEL_KEY::ICONSIZE], MODEL_KEY::ICONSIZE}, + {MODEL_NAME[MODEL_KEY::HIDDEN], MODEL_KEY::HIDDEN}, + {MODEL_NAME[MODEL_KEY::DETAILVIEW], MODEL_KEY::DETAILVIEW}, + {MODEL_NAME[MODEL_KEY::SHOWTERMINAL], MODEL_KEY::SHOWTERMINAL}, + {MODEL_NAME[MODEL_KEY::SHOWTHUMBNAIL], MODEL_KEY::SHOWTHUMBNAIL}, + {MODEL_NAME[MODEL_KEY::COUNT], MODEL_KEY::COUNT}, + {MODEL_NAME[MODEL_KEY::SORTBY], MODEL_KEY::SORTBY}, + {MODEL_NAME[MODEL_KEY::USER], MODEL_KEY::USER}, + {MODEL_NAME[MODEL_KEY::PASSWORD], MODEL_KEY::PASSWORD}, + {MODEL_NAME[MODEL_KEY::SERVER], MODEL_KEY::SERVER}, + {MODEL_NAME[MODEL_KEY::VIEWTYPE], MODEL_KEY::VIEWTYPE}, + {MODEL_NAME[MODEL_KEY::ADDDATE], MODEL_KEY::ADDDATE}, + {MODEL_NAME[MODEL_KEY::FAV], MODEL_KEY::FAV}, + {MODEL_NAME[MODEL_KEY::FAVORITE], MODEL_KEY::FAVORITE}, + {MODEL_NAME[MODEL_KEY::COLOR], MODEL_KEY::COLOR}, + {MODEL_NAME[MODEL_KEY::RATE], MODEL_KEY::RATE}, + {MODEL_NAME[MODEL_KEY::FORMAT], MODEL_KEY::FORMAT}, + {MODEL_NAME[MODEL_KEY::PLACE], MODEL_KEY::PLACE}, + {MODEL_NAME[MODEL_KEY::LOCATION], MODEL_KEY::LOCATION}, + {MODEL_NAME[MODEL_KEY::ALBUM], MODEL_KEY::ALBUM}, + {MODEL_NAME[MODEL_KEY::ARTIST], MODEL_KEY::ARTIST}, + {MODEL_NAME[MODEL_KEY::DURATION], MODEL_KEY::DURATION}, + {MODEL_NAME[MODEL_KEY::TRACK], MODEL_KEY::TRACK}, + {MODEL_NAME[MODEL_KEY::GENRE], MODEL_KEY::GENRE}, + {MODEL_NAME[MODEL_KEY::LYRICS], MODEL_KEY::LYRICS}, + {MODEL_NAME[MODEL_KEY::RELEASEDATE], MODEL_KEY::RELEASEDATE}, + {MODEL_NAME[MODEL_KEY::FORMAT], MODEL_KEY::FORMAT}, + {MODEL_NAME[MODEL_KEY::WIKI], MODEL_KEY::WIKI}, + {MODEL_NAME[MODEL_KEY::SOURCETYPE], MODEL_KEY::SOURCETYPE}, + {MODEL_NAME[MODEL_KEY::ARTWORK], MODEL_KEY::ARTWORK}, + {MODEL_NAME[MODEL_KEY::NOTE], MODEL_KEY::NOTE}, + {MODEL_NAME[MODEL_KEY::MOOD], MODEL_KEY::MOOD}, + {MODEL_NAME[MODEL_KEY::COMMENT], MODEL_KEY::COMMENT}, + {MODEL_NAME[MODEL_KEY::CONTEXT], MODEL_KEY::CONTEXT}, + {MODEL_NAME[MODEL_KEY::SOURCE], MODEL_KEY::SOURCE}, + {MODEL_NAME[MODEL_KEY::PLAYLIST], MODEL_KEY::PLAYLIST}, + {MODEL_NAME[MODEL_KEY::TITLE], MODEL_KEY::TITLE}, + {MODEL_NAME[MODEL_KEY::ID], MODEL_KEY::ID}, + {MODEL_NAME[MODEL_KEY::PARENT_ID], MODEL_KEY::PARENT_ID}, + {MODEL_NAME[MODEL_KEY::LICENSE], MODEL_KEY::LICENSE}, + {MODEL_NAME[MODEL_KEY::DESCRIPTION], MODEL_KEY::DESCRIPTION}, + {MODEL_NAME[MODEL_KEY::BOOKMARK], MODEL_KEY::BOOKMARK}, + {MODEL_NAME[MODEL_KEY::ACCOUNT], MODEL_KEY::ACCOUNT}, + {MODEL_NAME[MODEL_KEY::ACCOUNTTYPE], MODEL_KEY::ACCOUNTTYPE}, + {MODEL_NAME[MODEL_KEY::VERSION], MODEL_KEY::VERSION}, + {MODEL_NAME[MODEL_KEY::DOMAIN_M], MODEL_KEY::DOMAIN_M}, + {MODEL_NAME[MODEL_KEY::CATEGORY], MODEL_KEY::CATEGORY}, + {MODEL_NAME[MODEL_KEY::CONTENT], MODEL_KEY::CONTENT}, + {MODEL_NAME[MODEL_KEY::PIN], MODEL_KEY::PIN}, + {MODEL_NAME[MODEL_KEY::IMG], MODEL_KEY::IMG}, + {MODEL_NAME[MODEL_KEY::PREVIEW], MODEL_KEY::PREVIEW}, + {MODEL_NAME[MODEL_KEY::LINK], MODEL_KEY::LINK}, + {MODEL_NAME[MODEL_KEY::STAMP], MODEL_KEY::STAMP}, + {MODEL_NAME[MODEL_KEY::BOOK], MODEL_KEY::BOOK}, + + /** ccdav keys **/ + {MODEL_NAME[MODEL_KEY::N], MODEL_KEY::N}, + {MODEL_NAME[MODEL_KEY::IM], MODEL_KEY::IM}, + {MODEL_NAME[MODEL_KEY::PHOTO], MODEL_KEY::PHOTO}, + {MODEL_NAME[MODEL_KEY::GENDER], MODEL_KEY::GENDER}, + {MODEL_NAME[MODEL_KEY::ADR], MODEL_KEY::ADR}, + {MODEL_NAME[MODEL_KEY::ADR_2], MODEL_KEY::ADR_2}, + {MODEL_NAME[MODEL_KEY::ADR_3], MODEL_KEY::ADR_3}, + {MODEL_NAME[MODEL_KEY::EMAIL], MODEL_KEY::EMAIL}, + {MODEL_NAME[MODEL_KEY::EMAIL_2], MODEL_KEY::EMAIL_2}, + {MODEL_NAME[MODEL_KEY::EMAIL_3], MODEL_KEY::EMAIL_3}, + {MODEL_NAME[MODEL_KEY::LANG], MODEL_KEY::LANG}, + {MODEL_NAME[MODEL_KEY::NICKNAME], MODEL_KEY::NICKNAME}, + {MODEL_NAME[MODEL_KEY::ORG], MODEL_KEY::ORG}, + {MODEL_NAME[MODEL_KEY::PROFILE], MODEL_KEY::PROFILE}, + {MODEL_NAME[MODEL_KEY::TZ], MODEL_KEY::TZ}, + {MODEL_NAME[MODEL_KEY::TEL], MODEL_KEY::TEL}, + {MODEL_NAME[MODEL_KEY::TEL_2], MODEL_KEY::TEL_2}, + {MODEL_NAME[MODEL_KEY::TEL_3], MODEL_KEY::TEL_3}, + + {MODEL_NAME[MODEL_KEY::CITY], MODEL_KEY::CITY}, + {MODEL_NAME[MODEL_KEY::STATE], MODEL_KEY::STATE}, + {MODEL_NAME[MODEL_KEY::COUNTRY], MODEL_KEY::COUNTRY}, + + // opendesktop store keys + {MODEL_NAME[MODEL_KEY::PACKAGE_ARCH], MODEL_KEY::PACKAGE_ARCH}, + {MODEL_NAME[MODEL_KEY::PACKAGE_TYPE], MODEL_KEY::PACKAGE_TYPE}, + {MODEL_NAME[MODEL_KEY::GPG_FINGERPRINT], MODEL_KEY::GPG_FINGERPRINT}, + {MODEL_NAME[MODEL_KEY::GPG_SIGNATURE], MODEL_KEY::GPG_SIGNATURE}, + {MODEL_NAME[MODEL_KEY::PACKAGE_NAME], MODEL_KEY::PACKAGE_NAME}, + {MODEL_NAME[MODEL_KEY::PRICE], MODEL_KEY::PRICE}, + {MODEL_NAME[MODEL_KEY::REPOSITORY], MODEL_KEY::REPOSITORY}, + {MODEL_NAME[MODEL_KEY::TAGS], MODEL_KEY::TAGS}, + {MODEL_NAME[MODEL_KEY::WAY], MODEL_KEY::WAY}, + {MODEL_NAME[MODEL_KEY::PIC], MODEL_KEY::PIC}, + {MODEL_NAME[MODEL_KEY::SMALL_PIC], MODEL_KEY::SMALL_PIC}, + {MODEL_NAME[MODEL_KEY::CHANGED], MODEL_KEY::CHANGED}, + {MODEL_NAME[MODEL_KEY::COMMENTS], MODEL_KEY::COMMENTS}, + {MODEL_NAME[MODEL_KEY::CREATED], MODEL_KEY::CREATED}, + {MODEL_NAME[MODEL_KEY::DETAIL_PAGE], MODEL_KEY::DETAIL_PAGE}, + {MODEL_NAME[MODEL_KEY::DETAILS], MODEL_KEY::DETAILS}, + {MODEL_NAME[MODEL_KEY::TOTAL_DOWNLOADS], MODEL_KEY::TOTAL_DOWNLOADS}, + {MODEL_NAME[MODEL_KEY::GHNS_EXCLUDED], MODEL_KEY::GHNS_EXCLUDED}, + {MODEL_NAME[MODEL_KEY::LANGUAGE], MODEL_KEY::LANGUAGE}, + {MODEL_NAME[MODEL_KEY::PERSON_ID], MODEL_KEY::PERSON_ID}, + {MODEL_NAME[MODEL_KEY::SCORE], MODEL_KEY::SCORE}, + {MODEL_NAME[MODEL_KEY::SUMMARY], MODEL_KEY::SUMMARY}, + {MODEL_NAME[MODEL_KEY::TYPE_ID], MODEL_KEY::TYPE_ID}, + {MODEL_NAME[MODEL_KEY::TYPE_NAME], MODEL_KEY::TYPE_NAME}, + {MODEL_NAME[MODEL_KEY::XDG_TYPE], MODEL_KEY::XDG_TYPE}, + + // file props + {MODEL_NAME[MODEL_KEY::SYMLINK], MODEL_KEY::SYMLINK}, + {MODEL_NAME[MODEL_KEY::IS_SYMLINK], MODEL_KEY::IS_SYMLINK}, + {MODEL_NAME[MODEL_KEY::LAST_READ], MODEL_KEY::LAST_READ}, + {MODEL_NAME[MODEL_KEY::READABLE], MODEL_KEY::READABLE}, + {MODEL_NAME[MODEL_KEY::WRITABLE], MODEL_KEY::WRITABLE}, + {MODEL_NAME[MODEL_KEY::IS_DIR], MODEL_KEY::IS_DIR}, + {MODEL_NAME[MODEL_KEY::IS_FILE], MODEL_KEY::IS_FILE}, + {MODEL_NAME[MODEL_KEY::IS_REMOTE], MODEL_KEY::IS_REMOTE}, + {MODEL_NAME[MODEL_KEY::EXECUTABLE], MODEL_KEY::EXECUTABLE}, + {MODEL_NAME[MODEL_KEY::VALUE], MODEL_KEY::VALUE}, + {MODEL_NAME[MODEL_KEY::KEY], MODEL_KEY::KEY}, + + {MODEL_NAME[MODEL_KEY::MAC], MODEL_KEY::MAC}, + {MODEL_NAME[MODEL_KEY::LOT], MODEL_KEY::LOT}, + {MODEL_NAME[MODEL_KEY::APP], MODEL_KEY::APP}, + {MODEL_NAME[MODEL_KEY::URI], MODEL_KEY::URI}, + {MODEL_NAME[MODEL_KEY::DEVICE], MODEL_KEY::DEVICE}, + {MODEL_NAME[MODEL_KEY::LASTSYNC], MODEL_KEY::LASTSYNC}}; +/** + * @brief MODEL + */ +typedef QHash MODEL; + +/** + * @brief MODEL_LIST + */ +typedef QVector MODEL_LIST; + +/** + * @brief modelRoles + * @param model + * @return + */ +const QVector modelRoles(const MODEL &model); + +/** + * @brief mapValue + * @param map + * @param key + * @return + */ +const QString mapValue(const QVariantMap &map, const MODEL_KEY &key); + +/** + * @brief toMap + * @param model + * @return + */ +const QVariantMap toMap(const MODEL &model); + +/** + * @brief toModel + * @param map + * @return + */ +const MODEL toModel(const QVariantMap &map); + +/** + * Creates a MODEL_LIST from a QVariantList + * */ +/** + * @brief toModelList + * @param list + * @return + */ +const MODEL_LIST toModelList(const QVariantList &list); + +/** + * Creates a QVariantList from a MODEL_LIST + * */ +/** + * @brief toMapList + * @param list + * @return + */ +const QVariantList toMapList(const MODEL_LIST &list); + +/** + * Creates a new MODEL from another filtered by the given array of MODEL_KEY + * */ +/** + * @brief filterModel + * @param model + * @param keys + * @return + */ +const MODEL filterModel(const MODEL &model, const QVector &keys); + +/** + * Extracts from a MODEL_LIST the values from a given MODEL::KEY into a QStringList + * */ + +/** + * @brief modelToList + * @param list + * @param key + * @return + */ +const QStringList modelToList(const MODEL_LIST &list, const MODEL_KEY &key); + +/** + * @brief The PATH_CONTENT struct + */ +struct PATH_CONTENT { + QUrl path; // the url holding all the content + MODEL_LIST content; // the content from the url +}; + +/** + * @brief The PATHTYPE_KEY enum + */ +#if defined Q_OS_ANDROID || defined Q_OS_WIN || defined Q_OS_MACOS || defined Q_OS_IOS // for android, windows and mac use this for now +enum PATHTYPE_KEY : int { + PLACES_PATH, + REMOTE_PATH, + DRIVES_PATH, + REMOVABLE_PATH, + TAGS_PATH, + UNKNOWN_TYPE, + APPS_PATH, + TRASH_PATH, + SEARCH_PATH, + CLOUD_PATH, + FISH_PATH, + MTP_PATH, + QUICK_PATH, + BOOKMARKS_PATH, + OTHER_PATH, +}; +#else +enum PATHTYPE_KEY : int { + PLACES_PATH = KFilePlacesModel::GroupType::PlacesType, + REMOTE_PATH = KFilePlacesModel::GroupType::RemoteType, + DRIVES_PATH = KFilePlacesModel::GroupType::DevicesType, + REMOVABLE_PATH = KFilePlacesModel::GroupType::RemovableDevicesType, + TAGS_PATH = KFilePlacesModel::GroupType::TagsType, + UNKNOWN_TYPE = KFilePlacesModel::GroupType::UnknownType, + APPS_PATH = 9, + TRASH_PATH = 10, + SEARCH_PATH = 11, + CLOUD_PATH = 12, + FISH_PATH = 13, + MTP_PATH = 14, + QUICK_PATH = 15, + BOOKMARKS_PATH = 16, + OTHER_PATH = 17 +}; +#endif + +static const QHash PATHTYPE_SCHEME = {{PATHTYPE_KEY::PLACES_PATH, "file"}, + {PATHTYPE_KEY::BOOKMARKS_PATH, "file"}, + {PATHTYPE_KEY::DRIVES_PATH, "drives"}, + {PATHTYPE_KEY::APPS_PATH, "applications"}, + {PATHTYPE_KEY::REMOTE_PATH, "remote"}, + {PATHTYPE_KEY::REMOVABLE_PATH, "removable"}, + {PATHTYPE_KEY::UNKNOWN_TYPE, "Unkown"}, + {PATHTYPE_KEY::TRASH_PATH, "trash"}, + {PATHTYPE_KEY::TAGS_PATH, "tags"}, + {PATHTYPE_KEY::SEARCH_PATH, "search"}, + {PATHTYPE_KEY::CLOUD_PATH, "cloud"}, + {PATHTYPE_KEY::FISH_PATH, "fish"}, + {PATHTYPE_KEY::MTP_PATH, "mtp"}}; + +static const QHash PATHTYPE_SCHEME_NAME = {{PATHTYPE_SCHEME[PATHTYPE_KEY::PLACES_PATH], PATHTYPE_KEY::PLACES_PATH}, + {PATHTYPE_SCHEME[PATHTYPE_KEY::BOOKMARKS_PATH], PATHTYPE_KEY::BOOKMARKS_PATH}, + {PATHTYPE_SCHEME[PATHTYPE_KEY::DRIVES_PATH], PATHTYPE_KEY::DRIVES_PATH}, + {PATHTYPE_SCHEME[PATHTYPE_KEY::APPS_PATH], PATHTYPE_KEY::APPS_PATH}, + {PATHTYPE_SCHEME[PATHTYPE_KEY::REMOTE_PATH], PATHTYPE_KEY::REMOTE_PATH}, + {PATHTYPE_SCHEME[PATHTYPE_KEY::REMOVABLE_PATH], PATHTYPE_KEY::REMOVABLE_PATH}, + {PATHTYPE_SCHEME[PATHTYPE_KEY::UNKNOWN_TYPE], PATHTYPE_KEY::UNKNOWN_TYPE}, + {PATHTYPE_SCHEME[PATHTYPE_KEY::TRASH_PATH], PATHTYPE_KEY::TRASH_PATH}, + {PATHTYPE_SCHEME[PATHTYPE_KEY::TAGS_PATH], PATHTYPE_KEY::TAGS_PATH}, + {PATHTYPE_SCHEME[PATHTYPE_KEY::SEARCH_PATH], PATHTYPE_KEY::SEARCH_PATH}, + {PATHTYPE_SCHEME[PATHTYPE_KEY::CLOUD_PATH], PATHTYPE_KEY::CLOUD_PATH}, + {PATHTYPE_SCHEME[PATHTYPE_KEY::FISH_PATH], PATHTYPE_KEY::FISH_PATH}, + {PATHTYPE_SCHEME[PATHTYPE_KEY::MTP_PATH], PATHTYPE_KEY::MTP_PATH}}; + +static const QHash PATHTYPE_URI = {{PATHTYPE_KEY::PLACES_PATH, PATHTYPE_SCHEME[PATHTYPE_KEY::PLACES_PATH] + "://"}, + {PATHTYPE_KEY::BOOKMARKS_PATH, PATHTYPE_SCHEME[PATHTYPE_KEY::BOOKMARKS_PATH] + "://"}, + {PATHTYPE_KEY::DRIVES_PATH, PATHTYPE_SCHEME[PATHTYPE_KEY::DRIVES_PATH] + "://"}, + {PATHTYPE_KEY::APPS_PATH, PATHTYPE_SCHEME[PATHTYPE_KEY::APPS_PATH] + ":///"}, + {PATHTYPE_KEY::REMOTE_PATH, PATHTYPE_SCHEME[PATHTYPE_KEY::REMOTE_PATH] + "://"}, + {PATHTYPE_KEY::REMOVABLE_PATH, PATHTYPE_SCHEME[PATHTYPE_KEY::REMOVABLE_PATH] + "://"}, + {PATHTYPE_KEY::UNKNOWN_TYPE, PATHTYPE_SCHEME[PATHTYPE_KEY::UNKNOWN_TYPE] + "://"}, + {PATHTYPE_KEY::TRASH_PATH, PATHTYPE_SCHEME[PATHTYPE_KEY::TRASH_PATH] + "://"}, + {PATHTYPE_KEY::TAGS_PATH, PATHTYPE_SCHEME[PATHTYPE_KEY::TAGS_PATH] + ":///"}, + {PATHTYPE_KEY::SEARCH_PATH, PATHTYPE_SCHEME[PATHTYPE_KEY::SEARCH_PATH] + "://"}, + {PATHTYPE_KEY::CLOUD_PATH, PATHTYPE_SCHEME[PATHTYPE_KEY::CLOUD_PATH] + ":///"}, + {PATHTYPE_KEY::FISH_PATH, PATHTYPE_SCHEME[PATHTYPE_KEY::FISH_PATH] + "://"}, + {PATHTYPE_KEY::MTP_PATH, PATHTYPE_SCHEME[PATHTYPE_KEY::MTP_PATH] + "://"}}; + +static const QHash PATHTYPE_LABEL = {{PATHTYPE_KEY::PLACES_PATH, ("Places")}, + {PATHTYPE_KEY::BOOKMARKS_PATH, ("Bookmarks")}, + {PATHTYPE_KEY::DRIVES_PATH, ("Drives")}, + {PATHTYPE_KEY::APPS_PATH, ("Apps")}, + {PATHTYPE_KEY::REMOTE_PATH, ("Remote")}, + {PATHTYPE_KEY::REMOVABLE_PATH, ("Removable")}, + {PATHTYPE_KEY::UNKNOWN_TYPE, ("Unknown")}, + {PATHTYPE_KEY::TRASH_PATH, ("Trash")}, + {PATHTYPE_KEY::TAGS_PATH, ("Tags")}, + {PATHTYPE_KEY::SEARCH_PATH, ("Search")}, + {PATHTYPE_KEY::CLOUD_PATH, ("Cloud")}, + {PATHTYPE_KEY::FISH_PATH, ("Remote")}, + {PATHTYPE_KEY::MTP_PATH, ("Drives")}, + {PATHTYPE_KEY::OTHER_PATH, ("Others")}, + {PATHTYPE_KEY::QUICK_PATH, ("Quick")}}; + +/** + * @brief DataPath + */ +static const QString DataPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + +/** + * @brief ConfigPath + */ +static const QString ConfigPath = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).toString(); + +/** + * @brief CloudCachePath + */ +static const QString CloudCachePath = DataPath + "/Cloud/"; + +/** + * @brief DesktopPath + */ +static const QString DesktopPath = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)).toString(); + +/** + * @brief AppsPath + */ +static const QString AppsPath = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation)).toString(); + +/** + * @brief RootPath + */ +static const QString RootPath = QUrl::fromLocalFile("/").toString(); + +#if defined(Q_OS_ANDROID) +static const QString PicturesPath = QUrl::fromLocalFile(PATHS::PicturesPath).toString(); +static const QString DownloadsPath = QUrl::fromLocalFile(PATHS::DownloadsPath).toString(); +static const QString DocumentsPath = QUrl::fromLocalFile(PATHS::DocumentsPath).toString(); +static const QString HomePath = QUrl::fromLocalFile(PATHS::HomePath).toString(); +static const QString MusicPath = QUrl::fromLocalFile(PATHS::MusicPath).toString(); +static const QString VideosPath = QUrl::fromLocalFile(PATHS::VideosPath).toString(); + +static const QStringList defaultPaths = {HomePath, DocumentsPath, PicturesPath, MusicPath, VideosPath, DownloadsPath}; + +#else +static const QString PicturesPath = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)).toString(); +static const QString DownloadsPath = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)).toString(); +static const QString DocumentsPath = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)).toString(); +static const QString HomePath = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::HomeLocation)).toString(); +static const QString MusicPath = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::MusicLocation)).toString(); +static const QString VideosPath = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::MoviesLocation)).toString(); +static const QString TrashPath = QStringLiteral("trash:/"); + +static const QStringList defaultPaths = { + HomePath, + DesktopPath, + DocumentsPath, + PicturesPath, + MusicPath, + VideosPath, + DownloadsPath /*, + RootPath, + TrashPath*/ +}; + +#endif + +static const QMap folderIcon {{PicturesPath, "folder-pictures"}, + {DownloadsPath, "folder-download"}, + {DocumentsPath, "folder-documents"}, + {HomePath, "user-home"}, + {MusicPath, "folder-music"}, + {VideosPath, "folder-videos"}, + {DesktopPath, "user-desktop"}, + {AppsPath, "system-run"}, + {RootPath, "folder-root"}}; + +/** + * Checks if a local file exists. + * The URL must represent a local file path, by using the scheme file:// + **/ +/** + * @brief fileExists + * @param path + * @return + */ +bool fileExists(const QUrl &path); + +/** + * @brief fileDir + * @param path + * @return + */ +const QString fileDir(const QUrl &path); + +/** + * @brief parentDir + * @param path + * @return + */ +const QUrl parentDir(const QUrl &path); + +/** + * Return the configuration of a single directory represented + * by a QVariantMap. + * The passed path must be a local file URL. + **/ +/** + * @brief dirConf + * @param path + * @return + */ +const QVariantMap dirConf(const QUrl &path); + +/** + * @brief setDirConf + * @param path + * @param group + * @param key + * @param value + */ +void setDirConf(const QUrl &path, const QString &group, const QString &key, const QVariant &value); + +/** + * @brief getIconName + * Returns the icon name for certain file. + * The file path must be represented as a local file URL. + * It also looks into the directory config file to get custom set icons + * @param path + * @return + */ +const QString getIconName(const QUrl &path); + +/** + * @brief getMime + * @param path + * @return + */ +const QString getMime(const QUrl &path); + +/** + * @brief checkFileType + * @param type + * @param mimeTypeName + * @return + */ +bool checkFileType(const FILTER_TYPE &type, const QString &mimeTypeName); + +/** + * @brief thumbnailUrl + * Returns a valid thumbnail Url to an image provider if supported, otherwise an empty URL + * @param url + * @return + */ +const QUrl thumbnailUrl(const QUrl &url, const QString &mimetype); + +#if (!defined Q_OS_ANDROID && defined Q_OS_LINUX) || defined Q_OS_WIN +/** + * @brief packFileInfo + * @param item + * @return + */ +const MODEL getFileInfo(const KFileItem &kfile); +#endif + +/** + * @brief getFileInfoModel + * @param path + * @return + */ +const MODEL getFileInfoModel(const QUrl &path); + +/** + * @brief getFileInfo + * @param path + * @return + */ +const QVariantMap getFileInfo(const QUrl &path); + +/** + * @brief getDirInfoModel + * @param path + * @param type + * @return + */ +const MODEL getDirInfoModel(const QUrl &path, const QString &type = QString()); + +/** + * @brief getDirInfo + * @param path + * @param type + * @return + */ +const QVariantMap getDirInfo(const QUrl &path); + +/** + * @brief getPathType + * @param url + * @return + */ +PATHTYPE_KEY getPathType(const QUrl &url); +} + +#endif // FMH_H diff --git a/src/fmlist.cpp b/src/fmlist.cpp new file mode 100644 index 0000000..af5ba93 --- /dev/null +++ b/src/fmlist.cpp @@ -0,0 +1,626 @@ +/* + * + * Copyright (C) 2018 camilo higuita + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "fmlist.h" +#include "fm.h" + +#if defined Q_OS_LINUX && !defined Q_OS_ANDROID +#include +#endif + +#include +#include +#include +#include +#include + +FMList::FMList(QObject *parent) + : BaseList(parent) + , fm(new FM(this)) +{ + qRegisterMetaType("const FMList*"); // this is needed for QML to know of FMList in the search method + connect(this->fm, &FM::cloudServerContentReady, [&](const FMH::MODEL_LIST &list, const QUrl &url) { + if (this->path == url) { + this->assignList(list); + } + }); + + connect(this->fm, &FM::pathContentReady, [&](QUrl) { + emit this->preListChanged(); + this->sortList(); + this->setStatus({STATUS_CODE::READY, this->list.isEmpty() ? "Nothing here!" : "", this->list.isEmpty() ? "This place seems to be empty" : "", this->list.isEmpty() ? "folder-add" : "", this->list.isEmpty(), true}); + emit this->postListChanged(); + }); + + connect(this->fm, &FM::pathContentItemsChanged, [&](QVector> res) { + for (const auto &item : qAsConst(res)) { + const auto index = this->indexOf(FMH::MODEL_KEY::PATH, item.first[FMH::MODEL_KEY::PATH]); + + if (index >= this->list.size() || index < 0) + return; + + this->list[index] = item.second; + emit this->updateModel(index, FMH::modelRoles(item.second)); + } + }); + + connect(this->fm, &FM::pathContentItemsReady, [&](FMH::PATH_CONTENT res) { + if (res.path != this->path) + return; + + this->appendToList(res.content); + }); + + connect(this->fm, &FM::pathContentItemsRemoved, [&](FMH::PATH_CONTENT res) { + if (res.path != this->path) + return; + + if (!FMH::fileExists(res.path)) { + this->setStatus({STATUS_CODE::ERROR, "Error", "This URL cannot be listed", "documentinfo", true, false}); + return; + } + + for (const auto &item : qAsConst(res.content)) { + const auto index = this->indexOf(FMH::MODEL_KEY::PATH, item[FMH::MODEL_KEY::PATH]); + qDebug() << "SUPOSSED TO REMOVED THIS FORM THE LIST" << index << this->list.count() << item[FMH::MODEL_KEY::PATH]; + + this->remove(index); + } + + this->setStatus({STATUS_CODE::READY, this->list.isEmpty() ? "Nothing here!" : "", this->list.isEmpty() ? "This place seems to be empty" : "", this->list.isEmpty() ? "folder-add" : "", this->list.isEmpty(), true}); + }); + + connect(this->fm, &FM::warningMessage, [&](const QString &message) { + emit this->warning(message); + }); + + connect(this->fm, &FM::loadProgress, [&](const int &percent) { + emit this->progress(percent); + }); + + connect(this->fm, &FM::pathContentChanged, [&](const QUrl &path) { + qDebug() << "FOLDER PATH CHANGED" << path; + if (path != this->path) + return; + this->sortList(); + }); + + connect(this->fm, &FM::newItem, [&](const FMH::MODEL &item, const QUrl &url) { + if (this->path == url) { + emit this->preItemAppended(); + this->list << item; + emit this->postItemAppended(); + } + }); +} + +void FMList::assignList(const FMH::MODEL_LIST &list) +{ + emit this->preListChanged(); + this->list = list; + this->sortList(); + this->setStatus({STATUS_CODE::READY, this->list.isEmpty() ? "Nothing here!" : "", this->list.isEmpty() ? "This place seems to be empty" : "", this->list.isEmpty() ? "folder-add" : "", this->list.isEmpty(), true}); + emit this->postListChanged(); +} + +void FMList::appendToList(const FMH::MODEL_LIST &list) +{ + emit this->preItemsAppended(list.size()); + this->list << list; + emit this->postItemAppended(); +} + +void FMList::clear() +{ + emit this->preListChanged(); + this->list.clear(); + emit this->postListChanged(); +} + +void FMList::setList() +{ + qDebug() << "PATHTYPE FOR URL" << pathType << this->path.toString() << this->filters << this; + this->clear(); + + switch (this->pathType) { + case FMList::PATHTYPE::CLOUD_PATH: + this->fm->getCloudServerContent(this->path.toString(), this->filters, this->cloudDepth); + break; // ASYNC + + default: { + const bool exists = this->path.isLocalFile() ? FMH::fileExists(this->path) : true; + if (!exists) + this->setStatus({STATUS_CODE::ERROR, "Error", "This URL cannot be listed", "documentinfo", this->list.isEmpty(), exists}); + else { + this->fm->getPathContent(this->path, this->hidden, this->onlyDirs, QStringList() << this->filters << FMH::FILTER_LIST[static_cast(this->filterType)]); + } + break; // ASYNC + } + } +} + +void FMList::reset() +{ + this->setList(); +} + +const FMH::MODEL_LIST &FMList::items() const +{ + return this->list; +} + +FMList::SORTBY FMList::getSortBy() const +{ + return this->sort; +} + +void FMList::setSortBy(const FMList::SORTBY &key) +{ + if (this->sort == key) + return; + + emit this->preListChanged(); + + this->sort = key; + this->sortList(); + + emit this->sortByChanged(); + emit this->postListChanged(); +} + +void FMList::sortList() +{ + const FMH::MODEL_KEY key = static_cast(this->sort); + auto index = 0; + + if (this->foldersFirst) { + qSort(this->list.begin(), this->list.end(), [](const FMH::MODEL &e1, const FMH::MODEL &e2) -> bool { + Q_UNUSED(e2) + const auto key = FMH::MODEL_KEY::MIME; + return e1[key] == "inode/directory"; + }); + + for (const auto &item : qAsConst(this->list)) + if (item[FMH::MODEL_KEY::MIME] == "inode/directory") + index++; + else + break; + + std::sort(this->list.begin(), this->list.begin() + index, [&key](const FMH::MODEL &e1, const FMH::MODEL &e2) -> bool { + switch (key) { + case FMH::MODEL_KEY::SIZE: { + if (e1[key].toDouble() > e2[key].toDouble()) + return true; + break; + } + + case FMH::MODEL_KEY::MODIFIED: + case FMH::MODEL_KEY::DATE: { + auto currentTime = QDateTime::currentDateTime(); + + auto date1 = QDateTime::fromString(e1[key], Qt::TextDate); + auto date2 = QDateTime::fromString(e2[key], Qt::TextDate); + + if (date1.secsTo(currentTime) < date2.secsTo(currentTime)) + return true; + + break; + } + + case FMH::MODEL_KEY::LABEL: { + const auto str1 = QString(e1[key]).toLower(); + const auto str2 = QString(e2[key]).toLower(); + + if (str1 < str2) + return true; + break; + } + + default: + if (e1[key] < e2[key]) + return true; + } + + return false; + }); + } + + std::sort(this->list.begin() + index, this->list.end(), [key](const FMH::MODEL &e1, const FMH::MODEL &e2) -> bool { + switch (key) { + case FMH::MODEL_KEY::MIME: + if (e1[key] == "inode/directory") + return true; + break; + + case FMH::MODEL_KEY::SIZE: { + if (e1[key].toDouble() > e2[key].toDouble()) + return true; + break; + } + + case FMH::MODEL_KEY::MODIFIED: + case FMH::MODEL_KEY::DATE: { + auto currentTime = QDateTime::currentDateTime(); + + auto date1 = QDateTime::fromString(e1[key], Qt::TextDate); + auto date2 = QDateTime::fromString(e2[key], Qt::TextDate); + + if (date1.secsTo(currentTime) < date2.secsTo(currentTime)) + return true; + + break; + } + + case FMH::MODEL_KEY::LABEL: { + const auto str1 = QString(e1[key]).toLower(); + const auto str2 = QString(e2[key]).toLower(); + + if (str1 < str2) + return true; + break; + } + + default: + if (e1[key] < e2[key]) + return true; + } + + return false; + }); +} + +QString FMList::getPathName() const +{ + return this->pathName; +} + +QUrl FMList::getPath() const +{ + return this->path; +} + +void FMList::setPath(const QUrl &path) +{ + QUrl path_ = QUrl::fromUserInput(path.toString().trimmed()); + + if (this->path == path_) + return; + + this->path = path_; + m_navHistory.appendPath(this->path); + + this->setStatus({STATUS_CODE::LOADING, "Loading content", "Almost ready!", "view-refresh", true, false}); + + const auto __scheme = this->path.scheme(); + this->pathName = QDir(this->path.toLocalFile()).dirName(); + + if (__scheme == FMH::PATHTYPE_SCHEME[FMH::PATHTYPE_KEY::CLOUD_PATH]) { + this->pathType = FMList::PATHTYPE::CLOUD_PATH; + + } else if (__scheme == FMH::PATHTYPE_SCHEME[FMH::PATHTYPE_KEY::APPS_PATH]) { + this->pathType = FMList::PATHTYPE::APPS_PATH; + + } else if (__scheme == FMH::PATHTYPE_SCHEME[FMH::PATHTYPE_KEY::TAGS_PATH]) { + this->pathType = FMList::PATHTYPE::TAGS_PATH; + this->pathName = this->path.path(); + + } else if (__scheme == FMH::PATHTYPE_SCHEME[FMH::PATHTYPE_KEY::TRASH_PATH]) { + this->pathType = FMList::PATHTYPE::TRASH_PATH; + this->pathName = "Trash"; + + } else if (__scheme == FMH::PATHTYPE_SCHEME[FMH::PATHTYPE_KEY::PLACES_PATH]) { + this->pathType = FMList::PATHTYPE::PLACES_PATH; + + } else if (__scheme == FMH::PATHTYPE_SCHEME[FMH::PATHTYPE_KEY::MTP_PATH]) { + this->pathType = FMList::PATHTYPE::MTP_PATH; + + } else if (__scheme == FMH::PATHTYPE_SCHEME[FMH::PATHTYPE_KEY::FISH_PATH]) { + this->pathType = FMList::PATHTYPE::FISH_PATH; + + } else if (__scheme == FMH::PATHTYPE_SCHEME[FMH::PATHTYPE_KEY::REMOTE_PATH]) { + this->pathType = FMList::PATHTYPE::REMOTE_PATH; + + } else if (__scheme == FMH::PATHTYPE_SCHEME[FMH::PATHTYPE_KEY::DRIVES_PATH]) { + this->pathType = FMList::PATHTYPE::DRIVES_PATH; + } else { + this->pathType = FMList::PATHTYPE::OTHER_PATH; + } + + emit this->pathNameChanged(); + emit this->pathTypeChanged(); + emit this->pathChanged(); +} + +FMList::PATHTYPE FMList::getPathType() const +{ + return this->pathType; +} + +QStringList FMList::getFilters() const +{ + return this->filters; +} + +void FMList::setFilters(const QStringList &filters) +{ + if (this->filters == filters) + return; + + this->filters = filters; + + emit this->filtersChanged(); +} + +FMList::FILTER FMList::getFilterType() const +{ + return this->filterType; +} + +void FMList::setFilterType(const FMList::FILTER &type) +{ + if (this->filterType == type) + return; + + this->filterType = type; + + emit this->filterTypeChanged(); +} + +bool FMList::getHidden() const +{ + return this->hidden; +} + +void FMList::setHidden(const bool &state) +{ + if (this->hidden == state) + return; + + this->hidden = state; + + emit this->hiddenChanged(); +} + +bool FMList::getOnlyDirs() const +{ + return this->onlyDirs; +} + +void FMList::setOnlyDirs(const bool &state) +{ + if (this->onlyDirs == state) + return; + + this->onlyDirs = state; + + emit this->onlyDirsChanged(); +} + +void FMList::refresh() +{ + emit this->pathChanged(); +} + +void FMList::createDir(const QString &name) +{ + if (this->pathType == FMList::PATHTYPE::CLOUD_PATH) { +#ifdef COMPONENT_SYNCING + this->fm->createCloudDir(QString(this->path.toString()).replace(FMH::PATHTYPE_SCHEME[FMH::PATHTYPE_KEY::CLOUD_PATH] + "/" + this->fm->sync->getUser(), ""), name); +#endif + } else { + FMStatic::createDir(this->path, name); + } +} + +void FMList::copyInto(const QStringList &urls) +{ + this->fm->copy(QUrl::fromStringList(urls), this->path); +} + +void FMList::cutInto(const QStringList &urls) +{ + this->fm->cut(QUrl::fromStringList(urls), this->path); + // else if(this->pathType == FMList::PATHTYPE::CLOUD_PATH) + // { + // this->fm->createCloudDir(QString(this->path).replace(FMH::PATHTYPE_NAME[FMList::PATHTYPE::CLOUD_PATH]+"/"+this->fm->sync->getUser(), ""), name); + // } +} + +void FMList::setDirIcon(const int &index, const QString &iconName) +{ + if (index >= this->list.size() || index < 0) + return; + + // const auto index_ = this->mappedIndex(index); + + const auto path = QUrl(this->list.at(index)[FMH::MODEL_KEY::PATH]); + + if (!FMStatic::isDir(path)) + return; + + FMH::setDirConf(path.toString() + "/.directory", "Desktop Entry", "Icon", iconName); + + this->list[index][FMH::MODEL_KEY::ICON] = iconName; + emit this->updateModel(index, QVector {FMH::MODEL_KEY::ICON}); +} + +const QUrl FMList::getParentPath() +{ + switch (this->pathType) { + case FMList::PATHTYPE::PLACES_PATH: + return FMStatic::parentDir(this->path).toString(); + default: + return this->previousPath(); + } +} + +const QUrl FMList::posteriorPath() +{ + const auto url = m_navHistory.getPosteriorPath(); + + if (url.isEmpty()) + return this->path; + + return url; +} + +const QUrl FMList::previousPath() +{ + const auto url = m_navHistory.getPreviousPath(); + + if (url.isEmpty()) + return this->path; + + return url; +} + +bool FMList::getFoldersFirst() const +{ + return this->foldersFirst; +} + +void FMList::setFoldersFirst(const bool &value) +{ + if (this->foldersFirst == value) + return; + + emit this->preListChanged(); + + this->foldersFirst = value; + + emit this->foldersFirstChanged(); + + this->sortList(); + + emit this->postListChanged(); +} + +void FMList::search(const QString &query, const FMList *currentFMList) +{ + this->search(query, currentFMList->getPath(), currentFMList->getHidden(), currentFMList->getOnlyDirs(), currentFMList->getFilters()); +} + +void FMList::componentComplete() +{ + connect(this, &FMList::pathChanged, this, &FMList::setList); + connect(this, &FMList::filtersChanged, this, &FMList::setList); + connect(this, &FMList::filterTypeChanged, this, &FMList::setList); + connect(this, &FMList::hiddenChanged, this, &FMList::setList); + connect(this, &FMList::onlyDirsChanged, this, &FMList::setList); + + this->setList(); +} + +void FMList::search(const QString &query, const QUrl &path, const bool &hidden, const bool &onlyDirs, const QStringList &filters) +{ + qDebug() << "SEARCHING FOR" << query << path; + + if (!path.isLocalFile()) { + qWarning() << "URL recived is not a local file. So search will only filter the content" << path; + this->filterContent(query, path); + return; + } + + QFutureWatcher *watcher = new QFutureWatcher; + connect(watcher, &QFutureWatcher::finished, [=]() { + const auto res = watcher->future().result(); + + this->assignList(res.content); + emit this->searchResultReady(); + + watcher->deleteLater(); + }); + + QFuture t1 = QtConcurrent::run([=]() -> FMH::PATH_CONTENT { + FMH::PATH_CONTENT res; + res.path = path.toString(); + res.content = FMStatic::search(query, path, hidden, onlyDirs, filters); + return res; + }); + watcher->setFuture(t1); +} + +void FMList::filterContent(const QString &query, const QUrl &path) +{ + if (this->list.isEmpty()) { + qDebug() << "Can not filter content. List is empty"; + return; + } + + QFutureWatcher *watcher = new QFutureWatcher; + connect(watcher, &QFutureWatcher::finished, [=]() { + const auto res = watcher->future().result(); + + this->assignList(res.content); + emit this->searchResultReady(); + + watcher->deleteLater(); + }); + + QFuture t1 = QtConcurrent::run([=]() -> FMH::PATH_CONTENT { + FMH::MODEL_LIST m_content; + FMH::PATH_CONTENT res; + + for (const auto &item : qAsConst(this->list)) { + if (item[FMH::MODEL_KEY::LABEL].contains(query, Qt::CaseInsensitive) || item[FMH::MODEL_KEY::SUFFIX].contains(query, Qt::CaseInsensitive) || item[FMH::MODEL_KEY::MIME].contains(query, Qt::CaseInsensitive)) { + m_content << item; + } + } + + res.path = path.toString(); + res.content = m_content; + return res; + }); + watcher->setFuture(t1); +} + +int FMList::getCloudDepth() const +{ + return this->cloudDepth; +} + +void FMList::setCloudDepth(const int &value) +{ + if (this->cloudDepth == value) + return; + + this->cloudDepth = value; + + emit this->cloudDepthChanged(); +} + +PathStatus FMList::getStatus() const +{ + return this->m_status; +} + +void FMList::setStatus(const PathStatus &status) +{ + this->m_status = status; + emit this->statusChanged(); +} + +void FMList::remove(const int &index) +{ + if (index >= this->list.size() || index < 0) + return; + + emit this->preItemRemoved(index); + this->list.remove(index); + emit this->postItemRemoved(); +} diff --git a/src/fmlist.h b/src/fmlist.h new file mode 100644 index 0000000..1e312e2 --- /dev/null +++ b/src/fmlist.h @@ -0,0 +1,450 @@ +/* + * + * Copyright (C) 2018 Camilo Higuita + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FMLIST_H +#define FMLIST_H + +#include "fmh.h" +#include "baselist.h" +#include + +class FM; + +enum STATUS_CODE : uint_fast8_t { LOADING, ERROR, READY }; + +/** + * @brief The PathStatus class + * Represents the status of a directory, be it non existance, loading or empty. + */ +class PathStatus +{ + Q_GADGET + + Q_PROPERTY(STATUS_CODE code MEMBER m_code) + Q_PROPERTY(QString title MEMBER m_title) + Q_PROPERTY(QString message MEMBER m_message) + Q_PROPERTY(QString icon MEMBER m_icon) + Q_PROPERTY(bool empty MEMBER m_empty) + Q_PROPERTY(bool exists MEMBER m_exists) + +public: + STATUS_CODE m_code; + QString m_title; + QString m_message; + QString m_icon; + bool m_empty = false; + bool m_exists = false; +}; +Q_DECLARE_METATYPE(PathStatus) + +struct NavHistory { + void appendPath(const QUrl &path) + { + this->prev_history.append(path); + } + + QUrl getPosteriorPath() + { + if (this->post_history.isEmpty()) + return QUrl(); + + return this->post_history.takeLast(); + } + + QUrl getPreviousPath() + { + if (this->prev_history.isEmpty()) + return QUrl(); + + if (this->prev_history.length() < 2) + return this->prev_history.at(0); + + this->post_history.append(this->prev_history.takeLast()); + + return this->prev_history.takeLast(); + } + +private: + QVector prev_history; + QVector post_history; +}; + +/** + * @brief The FMList class + * Model for listing the file system files and directories and perfom relevant actions upon it + */ +class FMList : public BaseList +{ + Q_OBJECT + + // writable + Q_PROPERTY(QUrl path READ getPath WRITE setPath NOTIFY pathChanged) + Q_PROPERTY(bool hidden READ getHidden WRITE setHidden NOTIFY hiddenChanged) + Q_PROPERTY(bool onlyDirs READ getOnlyDirs WRITE setOnlyDirs NOTIFY onlyDirsChanged) + Q_PROPERTY(bool foldersFirst READ getFoldersFirst WRITE setFoldersFirst NOTIFY foldersFirstChanged) + Q_PROPERTY(int cloudDepth READ getCloudDepth WRITE setCloudDepth NOTIFY cloudDepthChanged) + + Q_PROPERTY(QStringList filters READ getFilters WRITE setFilters NOTIFY filtersChanged) + Q_PROPERTY(FMList::FILTER filterType READ getFilterType WRITE setFilterType NOTIFY filterTypeChanged) + Q_PROPERTY(FMList::SORTBY sortBy READ getSortBy WRITE setSortBy NOTIFY sortByChanged) + + // readonly + Q_PROPERTY(QString pathName READ getPathName NOTIFY pathNameChanged FINAL) + Q_PROPERTY(FMList::PATHTYPE pathType READ getPathType NOTIFY pathTypeChanged FINAL) + + Q_PROPERTY(PathStatus status READ getStatus NOTIFY statusChanged FINAL) + + Q_PROPERTY(QUrl parentPath READ getParentPath NOTIFY pathChanged) + +public: + enum SORTBY : uint_fast8_t { + SIZE = FMH::MODEL_KEY::SIZE, + MODIFIED = FMH::MODEL_KEY::MODIFIED, + DATE = FMH::MODEL_KEY::DATE, + LABEL = FMH::MODEL_KEY::LABEL, + MIME = FMH::MODEL_KEY::MIME, + ADDDATE = FMH::MODEL_KEY::MIME, + TITLE = FMH::MODEL_KEY::TITLE, + PLACE = FMH::MODEL_KEY::PLACE, + FORMAT = FMH::MODEL_KEY::FORMAT + + }; + Q_ENUM(SORTBY) + + enum FILTER : uint_fast8_t { + AUDIO = FMH::FILTER_TYPE::AUDIO, + VIDEO = FMH::FILTER_TYPE::VIDEO, + TEXT = FMH::FILTER_TYPE::TEXT, + IMAGE = FMH::FILTER_TYPE::IMAGE, + DOCUMENT = FMH::FILTER_TYPE::DOCUMENT, + COMPRESSED = FMH::FILTER_TYPE::COMPRESSED, + FONT = FMH::FILTER_TYPE::FONT, + NONE = FMH::FILTER_TYPE::NONE + }; + Q_ENUM(FILTER) + + enum PATHTYPE : uint_fast8_t { + PLACES_PATH = FMH::PATHTYPE_KEY::PLACES_PATH, + FISH_PATH = FMH::PATHTYPE_KEY::FISH_PATH, + MTP_PATH = FMH::PATHTYPE_KEY::MTP_PATH, + REMOTE_PATH = FMH::PATHTYPE_KEY::REMOTE_PATH, + DRIVES_PATH = FMH::PATHTYPE_KEY::DRIVES_PATH, + REMOVABLE_PATH = FMH::PATHTYPE_KEY::REMOVABLE_PATH, + TAGS_PATH = FMH::PATHTYPE_KEY::TAGS_PATH, + APPS_PATH = FMH::PATHTYPE_KEY::APPS_PATH, + TRASH_PATH = FMH::PATHTYPE_KEY::TRASH_PATH, + CLOUD_PATH = FMH::PATHTYPE_KEY::CLOUD_PATH, + QUICK_PATH = FMH::PATHTYPE_KEY::QUICK_PATH, + OTHER_PATH = FMH::PATHTYPE_KEY::OTHER_PATH + + }; + Q_ENUM(PATHTYPE) + + enum VIEW_TYPE : uint_fast8_t { + ICON_VIEW, + LIST_VIEW, + MILLERS_VIEW + + }; + Q_ENUM(VIEW_TYPE) + + Q_ENUM(STATUS_CODE) + + /** + * @brief FMList + * @param parent + */ + FMList(QObject *parent = nullptr); + + /** + * @brief items + * @return + */ + const FMH::MODEL_LIST &items() const final override; + + /** + * @brief getSortBy + * @return + */ + FMList::SORTBY getSortBy() const; + + /** + * @brief setSortBy + * @param key + */ + void setSortBy(const FMList::SORTBY &key); + + /** + * @brief componentComplete + */ + void componentComplete() override final; + + /** + * @brief getPath + * Current path being watched and model + * @return + * Directory URL + */ + QUrl getPath() const; + + /** + * @brief setPath + * Set the directory path to be model + * @param path + * Directory URL + */ + void setPath(const QUrl &path); + + /** + * @brief getPathName + * The short name of the current directory + * @return + */ + QString getPathName() const; + + /** + * @brief getPathType + * The type of the current path, be it LOCAl, TAGS, CLOUD, APPS, DEVICE or others + * @return + * Path type value + */ + FMList::PATHTYPE getPathType() const; + + /** + * @brief getFilters + * The filters being applied to the current directory + * @return + * List of filters + */ + QStringList getFilters() const; + + /** + * @brief setFilters + * FIlters to be applied as regular expressions + * @param filters + */ + void setFilters(const QStringList &filters); + + /** + * @brief getFilterType + * Filter typebeing applied, for example, filtering by AUDIO or IMAGES etc... + * @return + */ + FMList::FILTER getFilterType() const; + + /** + * @brief setFilterType + * Apply a filter type, this a quick shortcut for applying a filter on a file type such as AUDIO, IMAGE, DOCUMENT + * @param type + */ + void setFilterType(const FMList::FILTER &type); + + /** + * @brief getHidden + * Returns if the current model is including hidden files + * @return + */ + bool getHidden() const; + + /** + * @brief setHidden + * List hidden files in the model + * @param state + */ + void setHidden(const bool &state); + + /** + * @brief getOnlyDirs + * Returns if the current model is including only directories or not + * @return + */ + bool getOnlyDirs() const; + + /** + * @brief setOnlyDirs + * Only list directories when modeling a directory + * @param state + */ + void setOnlyDirs(const bool &state); + + /** + * @brief getParentPath + * Returns a URL to the parent directory of the current directory being modeled or the previous directory if the current URL is not a local file + * @return + */ + const QUrl getParentPath(); + + /** + * @brief getFoldersFirst + * Returns whether directories are listed first before other files + * @return + */ + bool getFoldersFirst() const; + + /** + * @brief setFoldersFirst + * List directories first + * @param value + */ + void setFoldersFirst(const bool &value); + + /** + * @brief getCloudDepth + * @return + */ + int getCloudDepth() const; + + /** + * @brief setCloudDepth + * @param value + */ + void setCloudDepth(const int &value); + + /** + * @brief getStatus + * Get the current status of the current path + * @return + */ + PathStatus getStatus() const; + +private: + FM *fm; + + void clear(); + void reset(); + void setList(); + void assignList(const FMH::MODEL_LIST &list); + void appendToList(const FMH::MODEL_LIST &list); + void sortList(); + void search(const QString &query, const QUrl &path, const bool &hidden = false, const bool &onlyDirs = false, const QStringList &filters = QStringList()); + void filterContent(const QString &query, const QUrl &path); + void setStatus(const PathStatus &status); + + FMH::MODEL_LIST list = {{}}; + + QUrl path; + QString pathName = QString(); + QStringList filters = {}; + + bool onlyDirs = false; + bool hidden = false; + + bool foldersFirst = false; + int cloudDepth = 1; + + PathStatus m_status; + + FMList::SORTBY sort = FMList::SORTBY::MODIFIED; + FMList::FILTER filterType = FMList::FILTER::NONE; + FMList::PATHTYPE pathType = FMList::PATHTYPE::PLACES_PATH; + + NavHistory m_navHistory; + +public slots: + + /** + * @brief refresh + * Refresh the model for new changes + */ + void refresh(); + + /** + * @brief createDir + * Create a new directory within the current directory + * @param name + * Name of the directory + */ + void createDir(const QString &name); + + /** + * @brief copyInto + * Copy a list of file URls into the current directory + * @param urls + * List of files + */ + void copyInto(const QStringList &urls); + + /** + * @brief cutInto + * Cut/move a list of file URLs to the current directory + * @param urls + * List of files + */ + void cutInto(const QStringList &urls); + + /** + * @brief setDirIcon + * Changes the icon of a directory by making use of the directory config file + * @param index + * Index of the directory in the model + * @param iconName + * Name of the new icon + */ + void setDirIcon(const int &index, const QString &iconName); + + /** + * @brief remove + * Remove an item from the model, this does not remove the file from the file system + * @param index + */ + void remove(const int &index); + + /** + * @brief search + * Perform a search on the current directory. The search is perfrom in another model than the current one + * @param query + * Query for the search + * @param currentFMList + * The information of the model where the search is going to be performed + */ + void search(const QString &query, const FMList *currentFMList); + + /** + * @brief previousPath + * Inmediate previous path + * @return + */ + const QUrl previousPath(); + + /** + * @brief posteriorPath + * Inmediate posterior path + * @return + */ + const QUrl posteriorPath(); + +signals: + void pathChanged(); + void pathNameChanged(); + void pathTypeChanged(); + void filtersChanged(); + void filterTypeChanged(); + void hiddenChanged(); + void onlyDirsChanged(); + void sortByChanged(); + void foldersFirstChanged(); + void statusChanged(); + void cloudDepthChanged(); + + void warning(QString message); + void progress(int percent); + + void searchResultReady(); +}; + +#endif // FMLIST_H diff --git a/src/fmstatic.cpp b/src/fmstatic.cpp new file mode 100644 index 0000000..1d0908b --- /dev/null +++ b/src/fmstatic.cpp @@ -0,0 +1,312 @@ +#include "fmstatic.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +FMStatic::FMStatic(QObject *parent) + : QObject(parent) +{ +} + +FMH::MODEL_LIST FMStatic::packItems(const QStringList &items, const QString &type) +{ + FMH::MODEL_LIST data; + + for (const auto &path : items) { + if (QUrl(path).isLocalFile() && !FMH::fileExists(path)) + continue; + + auto model = FMH::getFileInfoModel(path); + model.insert(FMH::MODEL_KEY::TYPE, type); + data << model; + } + + return data; +} + +FMH::MODEL_LIST FMStatic::getDefaultPaths() +{ + return FMStatic::packItems(FMH::defaultPaths, FMH::PATHTYPE_LABEL[FMH::PATHTYPE_KEY::PLACES_PATH]); +} + +FMH::MODEL_LIST FMStatic::search(const QString &query, const QUrl &path, const bool &hidden, const bool &onlyDirs, const QStringList &filters) +{ + FMH::MODEL_LIST content; + + if (!path.isLocalFile()) { + qWarning() << "URL recived is not a local file. FM::search" << path; + return content; + } + + if (FMStatic::isDir(path)) { + QDir::Filters dirFilter; + + dirFilter = (onlyDirs ? QDir::AllDirs | QDir::NoDotDot | QDir::NoDot : QDir::Files | QDir::AllDirs | QDir::NoDotDot | QDir::NoDot); + + if (hidden) + dirFilter = dirFilter | QDir::Hidden | QDir::System; + + QDirIterator it(path.toLocalFile(), filters, dirFilter, QDirIterator::Subdirectories); + while (it.hasNext()) { + auto url = it.next(); + if (it.fileName().contains(query, Qt::CaseInsensitive)) { + content << FMH::getFileInfoModel(QUrl::fromLocalFile(url)); + } + } + } else + qWarning() << "Search path does not exists" << path; + + qDebug() << content; + return content; +} + +FMH::MODEL_LIST FMStatic::getDevices() +{ + FMH::MODEL_LIST drives; + + return drives; +} + +QVariantMap FMStatic::getDirInfo(const QUrl &path) +{ + return FMH::getDirInfo(path); +} + +QVariantMap FMStatic::getFileInfo(const QUrl &path) +{ + return FMH::getFileInfo(path); +} + +bool FMStatic::isDefaultPath(const QString &path) +{ + return FMH::defaultPaths.contains(path); +} + +QUrl FMStatic::parentDir(const QUrl &path) +{ + return FMH::parentDir(path); +} + +bool FMStatic::isDir(const QUrl &path) +{ + if (!path.isLocalFile()) { + // qWarning() << "URL recived is not a local file. FM::isDir" << path; + return false; + } + + const QFileInfo file(path.toLocalFile()); + return file.isDir(); +} + +bool FMStatic::isCloud(const QUrl &path) +{ + return path.scheme() == FMH::PATHTYPE_SCHEME[FMH::PATHTYPE_KEY::CLOUD_PATH]; +} + +bool FMStatic::fileExists(const QUrl &path) +{ + return FMH::fileExists(path); +} + +QString FMStatic::fileDir(const QUrl &path) // the directory path of the file +{ + return FMH::fileDir(path); +} + +QString FMStatic::formatSize(const int &size) +{ + const QLocale locale; + return locale.formattedDataSize(size); +} + +QString FMStatic::formatDate(const QString &dateStr, const QString &format, const QString &initFormat) +{ + if (initFormat.isEmpty()) + return QDateTime::fromString(dateStr, Qt::TextDate).toString(format); + else + return QDateTime::fromString(dateStr, initFormat).toString(format); +} + +QString FMStatic::systemFormatDate(const QString &dateStr) +{ + return QLocale::system().toString(QDateTime::fromString(dateStr, Qt::TextDate), + QLocale::ShortFormat); +} + +QString FMStatic::formatTime(const qint64 &value) +{ + QString tStr; + if (value) { + QTime time((value / 3600) % 60, (value / 60) % 60, value % 60, (value * 1000) % 1000); + QString format = "mm:ss"; + if (value > 3600) + format = "hh:mm:ss"; + tStr = time.toString(format); + } + + return tStr.isEmpty() ? "00:00" : tStr; +} + +QString FMStatic::homePath() +{ + return FMH::HomePath; +} + +bool FMStatic::copy(const QList &urls, const QUrl &destinationDir) +{ + auto job = KIO::copy(urls, destinationDir); + job->start(); + return true; +} + +bool FMStatic::cut(const QList &urls, const QUrl &where) +{ + return FMStatic::cut(urls, where, QString()); +} + +bool FMStatic::cut(const QList &urls, const QUrl &where, const QString &name) +{ + QUrl _where = where; + if (!name.isEmpty()) + _where = QUrl(where.toString() + "/" + name); + + auto job = KIO::move(urls, _where, KIO::HideProgressInfo); + job->start(); + return true; +} + +bool FMStatic::removeFiles(const QList &urls) +{ + auto job = KIO::del(urls); + job->start(); + return true; +} + +void FMStatic::moveToTrash(const QList &urls) +{ + auto job = KIO::trash(urls); + job->start(); +} + +void FMStatic::emptyTrash() +{ + auto job = KIO::emptyTrash(); + job->start(); +} + +bool FMStatic::removeDir(const QUrl &path) +{ + bool result = true; + QDir dir(path.toLocalFile()); + qDebug() << "TRYING TO REMOVE DIR" << path << path.toLocalFile(); + if (dir.exists()) { + Q_FOREACH (QFileInfo info, dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)) { + if (info.isDir()) { + result = removeDir(QUrl::fromLocalFile(info.absoluteFilePath())); + } else { + result = QFile::remove(info.absoluteFilePath()); + } + + if (!result) { + return result; + } + } + result = dir.rmdir(path.toLocalFile()); + } + + return result; +} + +bool FMStatic::rename(const QUrl &url, const QString &name) +{ + return FMStatic::cut({url}, QUrl(url.toString().left(url.toString().lastIndexOf("/"))), name); +} + +bool FMStatic::createDir(const QUrl &path, const QString &name) +{ + auto job = KIO::mkdir(name.isEmpty() ? path : QUrl(path.toString() + "/" + name)); + job->start(); + return true; +} + +bool FMStatic::createFile(const QUrl &path, const QString &name) +{ + QFile file(path.toLocalFile() + "/" + name); + + if (file.open(QIODevice::ReadWrite)) { + file.close(); + return true; + } + + return false; +} + +bool FMStatic::createSymlink(const QUrl &path, const QUrl &where) +{ + qDebug() << "trying to create symlink" << path << where; + const auto job = KIO::link({path}, where); + job->start(); + return true; +} + +bool FMStatic::openUrl(const QUrl &url) +{ + KRun::runUrl(url, FMH::getFileInfoModel(url)[FMH::MODEL_KEY::MIME], nullptr, false, KRun::RunFlag::DeleteTemporaryFiles); + return true; +} + +void FMStatic::openLocation(const QStringList &urls) +{ + for (const auto &url : qAsConst(urls)) + QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(url).dir().absolutePath())); +} + +const QVariantMap FMStatic::dirConf(const QUrl &path) +{ + return FMH::dirConf(path); +} + +void FMStatic::setDirConf(const QUrl &path, const QString &group, const QString &key, const QVariant &value) +{ + FMH::setDirConf(path, group, key, value); +} + +bool FMStatic::checkFileType(const int &type, const QString &mimeTypeName) +{ + return FMH::checkFileType(static_cast(type), mimeTypeName); +} + +static bool doNameFilter(const QString &name, const QStringList &filters) +{ + const auto filtersAccumulate = std::accumulate(filters.constBegin(), filters.constEnd(), QVector {}, [](QVector &res, const QString &filter) -> QVector { + res.append(QRegExp(filter, Qt::CaseInsensitive, QRegExp::Wildcard)); + return res; + }); + + for (const auto &filter : filtersAccumulate) { + if (filter.exactMatch(name)) { + return true; + } + } + return false; +} + +QStringList FMStatic::nameFilters(const int &type) +{ + return FMH::FILTER_LIST[static_cast(type)]; +} + +QString FMStatic::iconName(const QString &value) +{ + return FMH::getIconName(value); +} diff --git a/src/fmstatic.h b/src/fmstatic.h new file mode 100644 index 0000000..d1c67f9 --- /dev/null +++ b/src/fmstatic.h @@ -0,0 +1,353 @@ +#ifndef FMSTATIC_H +#define FMSTATIC_H + +#include "fmh.h" +#include + +/** + * @brief The FMStatic class + * STatic file management methods, this class has a constructor only to register to QML, however all methods are static. + */ +class FMStatic : public QObject +{ + Q_OBJECT +public: + explicit FMStatic(QObject *parent = nullptr); + +public slots: + /** + * @brief search + * Search for files in a path using filters + * @param query + * Term to be searched, such as ".qml" or "music" + * @param path + * The path to perform the search upon + * @param hidden + * If should also search for hidden files + * @param onlyDirs + * If only searching for directories and not files + * @param filters + * List of filter patterns such as {"*.qml"}, it can use regular expressions + * @return + * The search results are returned as a FMH::MODEL_LIST + */ + static FMH::MODEL_LIST search(const QString &query, const QUrl &path, const bool &hidden = false, const bool &onlyDirs = false, const QStringList &filters = QStringList()); + + /** + * @brief getDevices + * Devices mounted to the file system + * @return + * Represented as a FMH::MODEL_LIST + */ + static FMH::MODEL_LIST getDevices(); + + /** + * @brief getDefaultPaths + * A model list of the default paths in most systems, such as Home, Pictures, Video, Downloads, Music and Documents folders + * @return + */ + static FMH::MODEL_LIST getDefaultPaths(); + + /** + * @brief packItems + * Given a list of path URLs pack all the info of such files as a FMH::MODEL_LIST + * @param items + * List of local URLs + * @param type + * The type of the list of urls, such as local, remote etc. This value is inserted with the key FMH::MODEL_KEY::TYPE + * @return + */ + static FMH::MODEL_LIST packItems(const QStringList &items, const QString &type); + + /** + * @brief copy + * Perfom a copy of the files to the passed destination + * @param urls + * List of URLs to be copy + * @param destinationDir + * Destination + * @return + * Return if the operation has been succesfull + */ + static bool copy(const QList &urls, const QUrl &destinationDir); + + /** + * @brief cut + * Perform a move/cut of a list of files to a destination. This function also moves the associated tags if the tags component has been enabled COMPONENT_TAGGING + * @param urls + * List of URLs to be moved + * @param where + * Destination path + * @return + * If the operation has been sucessfull + */ + static bool cut(const QList &urls, const QUrl &where); + + /** + * @brief cut + * @param urls + * @param where + * @param name + * New name of the files to be moved + * @return + */ + static bool cut(const QList &urls, const QUrl &where, const QString &name); + + /** + * @brief removeFiles + * List of files to be removed completely. This function also removes the assciated tags to the files if the tagging component has been enabled COMPONENT_TAGGING + * @param urls + * @return + * If the operation has been sucessfull + */ + static bool removeFiles(const QList &urls); + + /** + * @brief removeDir + * Remove a directory recursively + * @param path + * Path URL to be rmeoved + * @return + * If the operation has been sucessfull + */ + static bool removeDir(const QUrl &path); + + /** + * @brief formatSize + * Format a file size + * @param size + * size in bytes + * @return + * Formated into a readable string + */ + static QString formatSize(const int &size); + + /** + * @brief formatTime + * Format a milliseconds value to a readable format + * @param value + * Milliseconds + * @return + * Readable formated value + */ + static QString formatTime(const qint64 &value); + + /** + * @brief formatDate + * Given a date string, a format and a intended format return a readable string + * @param dateStr + * Date format + * @param format + * Intended format, by default "dd/MM/yyyy" + * @param initFormat + * Date format + * @return + */ + static QString formatDate(const QString &dateStr, const QString &format = QString("dd/MM/yyyy"), const QString &initFormat = QString()); + + static QString systemFormatDate(const QString &dateStr); + + /** + * @brief homePath + * The default home path in different systems + * @return + */ + static QString homePath(); + + /** + * @brief parentDir + * Given a file url return its parent directory + * @param path + * The file URL + * @return + * The parent directory URL if it exists otherwise returns the passed URL + */ + static QUrl parentDir(const QUrl &path); + + /** + * @brief getDirInfo + * Get info of a directory packed as a QVariantMap model + * @param path + * Path URL + * @return + */ + static QVariantMap getDirInfo(const QUrl &path); + + /** + * @brief getFileInfo + * Get file info + * @param path + * @return + * File info packed as a QVariantMap model + */ + static QVariantMap getFileInfo(const QUrl &path); + + /** + * @brief isDefaultPath + * Checks if a given path URL is a default path as in returned by the defaultPaths method + * @param path + * @return + */ + static bool isDefaultPath(const QString &path); + + /** + * @brief isDir + * If a local file URL is a directory + * @param path + * File URL + * @return + */ + static bool isDir(const QUrl &path); + + /** + * @brief isCloud + * If a path is a URL server instead of a local file + * @param path + * @return + */ + static bool isCloud(const QUrl &path); + + /** + * @brief fileExists + * Checks if a local file exists in the file system + * @param path + * File URL + * @return + * Existance + */ + static bool fileExists(const QUrl &path); + + /** + * if the url is a file path then it returns its directory + * and if it is a directory returns the same path + * */ + /** + * @brief fileDir + * Gives the directory URL path of a file, and if it is a directory returns the same path + * @param path + * File path URL + * @return + * The directory URL + */ + static QString fileDir(const QUrl &path); + + /** + * @brief dirConf + * The config values of a directory, such values can be any from iconname to specific ones. The config file is stored in the directory as .dir + * @param path + * @return + */ + static const QVariantMap dirConf(const QUrl &path); + + /** + * @brief setDirConf + * Write a config key-value to the directory config file + * @param path + * @param group + * @param key + * @param value + */ + static void setDirConf(const QUrl &path, const QString &group, const QString &key, const QVariant &value); + + /** + * @brief checkFileType + * Checks if a mimetype belongs to a file type, for example image/jpg belong to the type FMH::FILTER_TYPE + * @param type + * FMH::FILTER_TYPE value + * @param mimeTypeName + * @return + */ + static bool checkFileType(const int &type, const QString &mimeTypeName); + + /** + * @brief moveToTrash + * Moves to the trash can the file URLs. The associated tags are kept in case the files are restored. + * @param urls + */ + static void moveToTrash(const QList &urls); + + /** + * @brief emptyTrash + * Empty the trash casn + */ + static void emptyTrash(); + + /** + * @brief rename + * Rename a file to a new name + * @param url + * File URL to be renamed + * @param name + * The short new name of the file, not the new URL, for setting a new URl use cut instead. + * @return + */ + static bool rename(const QUrl &url, const QString &name); + + /** + * @brief createDir + * Creates a directory given a base path and a directory name + * @param path + * Base directory path + * @param name + * New directory name + * @return + * If the operation was sucessfull + */ + static bool createDir(const QUrl &path, const QString &name); + + /** + * @brief createFile + * Creates a file given the base directory path and a short file name + * @param path + * Base directory path + * @param name + * Name of the new file to be created with the extension + * @return + */ + static bool createFile(const QUrl &path, const QString &name); + + /** + * @brief createSymlink + * Creates a symlink + * @param path + * File to be symlinked + * @param where + * Destination of the symlink + * @return + */ + static bool createSymlink(const QUrl &path, const QUrl &where); + + /** + * @brief openUrl + * Given a URL it tries to open it using the default app associated to it + * @param url + * The URL to be open + * @return + */ + static bool openUrl(const QUrl &url); + + /** + * @brief openLocation + * Open with the default file manager a list of URLs + * @param urls + */ + static void openLocation(const QStringList &urls); + + /** + * @brief nameFilters + * Given a filter type return a list of associated name filters, as in suffixes. + * @param type + * The filter type to be mapped to a FMH::FILTER_TYPE + */ + static QStringList nameFilters(const int &type); + + /** + * @brief iconName + * Get the icon name associated to the file or name. + * @param value + * The file path or file name + */ + static QString iconName(const QString &value); +}; + +#endif // FMSTATIC_H diff --git a/src/handy.cpp b/src/handy.cpp new file mode 100644 index 0000000..2554e13 --- /dev/null +++ b/src/handy.cpp @@ -0,0 +1,113 @@ +/* + * Copyright 2018 Camilo Higuita + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "handy.h" +#include "fmh.h" + +#include +#include +#include +#include +#include +#include +#include + +Handy::Handy(QObject *parent) + : QObject(parent) +{ + +} + +QVariantMap Handy::userInfo() +{ + QString name = qgetenv("USER"); + if (name.isEmpty()) + name = qgetenv("USERNAME"); + + return QVariantMap({{FMH::MODEL_NAME[FMH::MODEL_KEY::NAME], name}}); +} + +QString Handy::getClipboardText() +{ + auto clipbopard = QApplication::clipboard(); + + auto mime = clipbopard->mimeData(); + if (mime->hasText()) + return clipbopard->text(); + + return QString(); +} + +QVariantMap Handy::getClipboard() +{ + QVariantMap res; + + auto clipboard = QApplication::clipboard(); + + auto mime = clipboard->mimeData(); + if (mime->hasUrls()) + res.insert("urls", QUrl::toStringList(mime->urls())); + + if (mime->hasText()) + res.insert("text", mime->text()); + + const QByteArray a = mime->data(QStringLiteral("application/x-kde-cutselection")); + res.insert("cut", (!a.isEmpty() && a.at(0) == '1')); + return res; +} + +bool Handy::copyToClipboard(const QVariantMap &value, const bool &cut) +{ + auto clipboard = QApplication::clipboard(); + QMimeData *mimeData = new QMimeData(); + + if (value.contains("urls")) + mimeData->setUrls(QUrl::fromStringList(value["urls"].toStringList())); + + if (value.contains("text")) + mimeData->setText(value["text"].toString()); + + mimeData->setData(QStringLiteral("application/x-kde-cutselection"), cut ? "1" : "0"); + clipboard->setMimeData(mimeData); + + return true; +} + +void Handy::setAsWallpaper(const QUrl &url) +{ + if (!url.isLocalFile()) + return; + + QDBusInterface iface("org.cutefish.Settings", "/Theme", + "org.cutefish.Theme", + QDBusConnection::sessionBus(), nullptr); + if (iface.isValid()) + iface.call("setWallpaper", url.toLocalFile()); +} + +bool Handy::copyTextToClipboard(const QString &text) +{ + QApplication::clipboard()->setText(text); + return true; +} + +int Handy::version() +{ + return QOperatingSystemVersion::current().majorVersion(); +} diff --git a/src/handy.h b/src/handy.h new file mode 100644 index 0000000..3794631 --- /dev/null +++ b/src/handy.h @@ -0,0 +1,81 @@ +/* + * Copyright 2018 Camilo Higuita + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef HANDY_H +#define HANDY_H + +#include + +#include + +/*! + * \brief The Handy class + * Contains useful static methods to be used as an attached property to the Maui application + */ +class Handy : public QObject +{ + Q_OBJECT + +public: + Handy(QObject *parent = nullptr); + +public slots: + /*! + * \brief Returns the major version of the current OS + * + * This function is static. + * \return Major OS version + */ + static int version(); + + /*! + * \brief Returns a QVariantMap containing basic information about the current user + * + * The pairs keys for the information returned are: + * "name" + * \return QVariantMap with user info + */ + static QVariantMap userInfo(); + + /*! + * \brief Returns the text contained in the clipboard + * \return QString containing clipboard text + */ + static QString getClipboardText(); + static QVariantMap getClipboard(); + + /*! + * \brief Copies text to the clipboard + * \param text text to be copied to the clipboard + * \return + */ + static bool copyTextToClipboard(const QString &text); + + /** + * @brief copyToClipboard + * @param value + * @param cut + * @return + */ + static bool copyToClipboard(const QVariantMap &value, const bool &cut = false); + + static void setAsWallpaper(const QUrl &url); +}; + +#endif // HANDY_H diff --git a/src/iconthemeprovider.cpp b/src/iconthemeprovider.cpp new file mode 100644 index 0000000..7cb0312 --- /dev/null +++ b/src/iconthemeprovider.cpp @@ -0,0 +1,33 @@ +#include "iconthemeprovider.h" +#include + +IconThemeProvider::IconThemeProvider() + : QQuickImageProvider(QQuickImageProvider::Pixmap) +{ +} + +QPixmap IconThemeProvider::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/iconthemeprovider.h b/src/iconthemeprovider.h new file mode 100644 index 0000000..1f62f86 --- /dev/null +++ b/src/iconthemeprovider.h @@ -0,0 +1,14 @@ +#ifndef ICONTHEMEPROVIDER_H +#define ICONTHEMEPROVIDER_H + +#include + +class IconThemeProvider : public QQuickImageProvider +{ +public: + IconThemeProvider(); + + QPixmap requestPixmap(const QString &id, QSize *realSize, const QSize &requestedSize); +}; + +#endif // ICONTHEMEPROVIDER_H diff --git a/src/lib/fileitemactions.cpp b/src/lib/fileitemactions.cpp new file mode 100644 index 0000000..88887a2 --- /dev/null +++ b/src/lib/fileitemactions.cpp @@ -0,0 +1,20 @@ +#include "fileitemactions.h" +#include + +FileItemActions::FileItemActions(QObject *parent) + : QObject(parent) +{ + +} + +KService::List FileItemActions::associatedApplications(const QStringList &mimeTypeList, const QString &traderConstraint) +{ + const KService::List firstOffers = KMimeTypeTrader::self()->query(mimeTypeList.first(), "Application", traderConstraint); + QStringList serviceList; + + for (int i = 0; i < firstOffers.count(); ++i) { + + } + + return KService::List(); +} diff --git a/src/lib/fileitemactions.h b/src/lib/fileitemactions.h new file mode 100644 index 0000000..d366390 --- /dev/null +++ b/src/lib/fileitemactions.h @@ -0,0 +1,18 @@ +#ifndef FILEITEMACTIONS_H +#define FILEITEMACTIONS_H + +#include +#include + +class FileItemActions : public QObject +{ + Q_OBJECT + +public: + explicit FileItemActions(QObject *parent = nullptr); + + static KService::List associatedApplications(const QStringList& mimeTypeList, const QString& traderConstraint); + +}; + +#endif // FILEITEMACTIONS_H diff --git a/src/lib/foldermodel.cpp b/src/lib/foldermodel.cpp new file mode 100644 index 0000000..e428352 --- /dev/null +++ b/src/lib/foldermodel.cpp @@ -0,0 +1,2156 @@ +/*************************************************************************** + * Copyright (C) 2006 David Faure * + * Copyright (C) 2008 Fredrik Höglund * + * Copyright (C) 2008 Rafael Fernández López * + * Copyright (C) 2011 Marco Martin * + * Copyright (C) 2014 by Eike Hein * + * Copyright (C) 2021 by Reven Martin * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +#include "foldermodel.h" +#include "itemviewadapter.h" +#include "positioner.h" +#include "../dialogs/propertiesdialog.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +Q_LOGGING_CATEGORY(FOLDERMODEL, "cutefish.desktop.folder.foldermodel") + +DirLister::DirLister(QObject *parent) + : KDirLister(parent) +{ +} + +DirLister::~DirLister() +{ +} + +void DirLister::handleError(KIO::Job *job) +{ + if (!autoErrorHandlingEnabled()) { + emit error(job->errorString()); + return; + } + + KDirLister::handleError(job); +} + +FolderModel::FolderModel(QObject *parent) + : QSortFilterProxyModel(parent) + , m_dirWatch(nullptr) + , m_dragInProgress(false) + , m_urlChangedWhileDragging(false) + , m_dropTargetPositionsCleanup(new QTimer(this)) + , m_previewGenerator(nullptr) + , m_viewAdapter(nullptr) + , m_actionCollection(this) + , m_newMenu(nullptr) + , m_fileItemActions(nullptr) + , m_usedByContainment(false) + , m_locked(true) + , m_sortMode(0) + , m_sortDesc(false) + , m_sortDirsFirst(true) + , m_parseDesktopFiles(false) + , m_previews(false) + , m_filterMode(NoFilter) + , m_filterPatternMatchAll(true) + , m_screenUsed(false) + // , m_screenMapper(ScreenMapper::instance()) + , m_complete(false) +{ + // needed to pass the job around with qml + // qmlRegisterType(); + + qmlRegisterAnonymousType("Cutefish.FileManager", 1); + + DirLister *dirLister = new DirLister(this); + dirLister->setDelayedMimeTypes(true); + dirLister->setAutoErrorHandlingEnabled(false, nullptr); + + connect(dirLister, &DirLister::error, this, &FolderModel::dirListFailed); + connect(dirLister, &KCoreDirLister::itemsDeleted, this, &FolderModel::evictFromIsDirCache); + connect(dirLister, &KCoreDirLister::started, this, std::bind(&FolderModel::setStatus, this, Status::Listing)); + + void (KCoreDirLister::*myCompletedSignal)() = &KCoreDirLister::completed; + QObject::connect(dirLister, myCompletedSignal, this, [this] { + setStatus(Status::Ready); + emit listingCompleted(); + }); + + void (KCoreDirLister::*myCanceledSignal)() = &KCoreDirLister::canceled; + QObject::connect(dirLister, myCanceledSignal, this, [this] { + setStatus(Status::Canceled); + emit listingCanceled(); + }); + + m_dirModel = new KDirModel(this); + m_dirModel->setDirLister(dirLister); + m_dirModel->setDropsAllowed(KDirModel::DropOnDirectory | KDirModel::DropOnLocalExecutable); + + // If we have dropped items queued for moving, go unsorted now. + connect(this, &QAbstractItemModel::rowsAboutToBeInserted, this, [this]() { + if (!m_dropTargetPositions.isEmpty()) { + setSortMode(-1); + } + }); + + // Position dropped items at the desired target position. + connect(this, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) { + for (int i = first; i <= last; ++i) { + const auto idx = index(i, 0, parent); + const auto url = itemForIndex(idx).url(); + auto it = m_dropTargetPositions.find(url.fileName()); + if (it != m_dropTargetPositions.end()) { + const auto pos = it.value(); + m_dropTargetPositions.erase(it); + emit move(pos.x(), pos.y(), {url}); + } + } + }); + + /* + * Dropped files may not actually show up as new files, e.g. when we overwrite + * an existing file. Or files that fail to be listed by the dirLister, or... + * To ensure we don't grow the map indefinitely, clean it up periodically. + * The cleanup timer is (re)started whenever we modify the map. We use a quite + * high interval of 10s. This should ensure, that we don't accidentally wipe + * the mapping when we actually still want to use it. Since the time between + * adding an entry in the map and it showing up in the model should be + * small, this should rarely, if ever happen. + */ + m_dropTargetPositionsCleanup->setInterval(10000); + m_dropTargetPositionsCleanup->setSingleShot(true); + connect(m_dropTargetPositionsCleanup, &QTimer::timeout, this, [this]() { + if (!m_dropTargetPositions.isEmpty()) { + qCDebug(FOLDERMODEL) << "clearing drop target positions after timeout:" << m_dropTargetPositions; + m_dropTargetPositions.clear(); + } + }); + + m_selectionModel = new QItemSelectionModel(this, this); + connect(m_selectionModel, &QItemSelectionModel::selectionChanged, this, &FolderModel::selectionChanged); + + setSourceModel(m_dirModel); + + setSortLocaleAware(true); + setFilterCaseSensitivity(Qt::CaseInsensitive); + setDynamicSortFilter(true); + + sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder); + + createActions(); +} + +FolderModel::~FolderModel() +{ +// if (m_usedByContainment) { +// // disconnect so we don't handle signals from the screen mapper when +// // removeScreen is called +// m_screenMapper->disconnect(this); +// m_screenMapper->removeScreen(m_screen, resolvedUrl()); +// } +} + +QHash FolderModel::roleNames() const +{ + return staticRoleNames(); +} + +QHash FolderModel::staticRoleNames() +{ + QHash roleNames; + roleNames[Qt::DisplayRole] = "display"; + roleNames[Qt::DecorationRole] = "decoration"; + roleNames[BlankRole] = "blank"; + roleNames[OverlaysRole] = "overlays"; + roleNames[SelectedRole] = "selected"; + roleNames[IsDirRole] = "isDir"; + roleNames[IsLinkRole] = "isLink"; + roleNames[IsHiddenRole] = "isHidden"; + roleNames[UrlRole] = "url"; + roleNames[LinkDestinationUrl] = "linkDestinationUrl"; + roleNames[SizeRole] = "size"; + roleNames[TypeRole] = "type"; + + return roleNames; +} + +void FolderModel::classBegin() +{ +} + +void FolderModel::componentComplete() +{ + m_complete = true; + invalidate(); +} + +void FolderModel::invalidateIfComplete() +{ + if (!m_complete) { + return; + } + + invalidate(); +} + +void FolderModel::invalidateFilterIfComplete() +{ + if (!m_complete) { + return; + } + + invalidateFilter(); +} + +void FolderModel::newFileMenuItemCreated(const QUrl &url) +{ + Q_UNUSED(url) + +// if (m_usedByContainment && !m_screenMapper->sharedDesktops()) { +// m_screenMapper->addMapping(url, m_screen, ScreenMapper::DelayedSignal); +// m_dropTargetPositions.insert(url.fileName(), m_menuPosition); +// m_menuPosition = {}; +// m_dropTargetPositionsCleanup->start(); +// } +} + +QString FolderModel::url() const +{ + return m_url; +} + +void FolderModel::setUrl(const QString &url) +{ + const QUrl &resolvedNewUrl = resolve(url); + + if (url == m_url) { + m_dirModel->dirLister()->updateDirectory(resolvedNewUrl); + return; + } + + const auto oldUrl = resolvedUrl(); + + beginResetModel(); + m_url = url; + m_isDirCache.clear(); + m_dirModel->dirLister()->openUrl(resolvedNewUrl); + clearDragImages(); + m_dragIndexes.clear(); + endResetModel(); + + emit urlChanged(); + emit resolvedUrlChanged(); + + m_errorString.clear(); + emit errorStringChanged(); + + if (m_dirWatch) { + delete m_dirWatch; + m_dirWatch = nullptr; + } + + if (resolvedNewUrl.isValid()) { + m_dirWatch = new KDirWatch(this); + connect(m_dirWatch, &KDirWatch::created, this, &FolderModel::iconNameChanged); + connect(m_dirWatch, &KDirWatch::dirty, this, &FolderModel::iconNameChanged); + m_dirWatch->addFile(resolvedNewUrl.toLocalFile() + QLatin1String("/.directory")); + } + + if (m_dragInProgress) { + m_urlChangedWhileDragging = true; + } + + emit iconNameChanged(); + +// if (m_usedByContainment && !m_screenMapper->sharedDesktops()) { +// m_screenMapper->removeScreen(m_screen, oldUrl); +// m_screenMapper->addScreen(m_screen, resolvedUrl()); +// } +} + +QUrl FolderModel::resolvedUrl() const +{ + return m_dirModel->dirLister()->url(); +} + +QUrl FolderModel::resolve(const QString &url) +{ + QUrl resolvedUrl; + + if (url.startsWith(QLatin1Char('~'))) { + resolvedUrl = QUrl::fromLocalFile(KShell::tildeExpand(url)); + } else { + resolvedUrl = QUrl::fromUserInput(url); + } + + return resolvedUrl; +} + +QString FolderModel::iconName() const +{ + const KFileItem rootItem(m_dirModel->dirLister()->url()); + + if (!rootItem.isFinalIconKnown()) { + rootItem.determineMimeType(); + } + + return rootItem.iconName(); +} + +FolderModel::Status FolderModel::status() const +{ + return m_status; +} + +void FolderModel::setStatus(Status status) +{ + if (m_status != status) { + m_status = status; + emit statusChanged(); + } +} + +QString FolderModel::errorString() const +{ + return m_errorString; +} + +bool FolderModel::dragging() const +{ + return m_dragInProgress; +} + +bool FolderModel::usedByContainment() const +{ + return m_usedByContainment; +} + +void FolderModel::setUsedByContainment(bool used) +{ + Q_UNUSED(used) + +// if (m_usedByContainment != used) { +// m_usedByContainment = used; + +// QAction *action = m_actionCollection.action(QStringLiteral("refresh")); + +// if (action) { +// action->setText(m_usedByContainment ? tr("&Refresh Desktop") : tr("&Refresh View")); +// action->setIcon(m_usedByContainment ? QIcon::fromTheme(QStringLiteral("user-desktop")) : QIcon::fromTheme(QStringLiteral("view-refresh"))); +// } + +// m_screenMapper->disconnect(this); +// connect(m_screenMapper, &ScreenMapper::screensChanged, this, &FolderModel::invalidateFilterIfComplete); +// connect(m_screenMapper, &ScreenMapper::screenMappingChanged, this, &FolderModel::invalidateFilterIfComplete); + +// emit usedByContainmentChanged(); +// } +} + +bool FolderModel::locked() const +{ + return m_locked; +} + +void FolderModel::setLocked(bool locked) +{ + if (m_locked != locked) { + m_locked = locked; + + emit lockedChanged(); + } +} + +void FolderModel::dirListFailed(const QString &error) +{ + m_errorString = error; + emit errorStringChanged(); +} + +int FolderModel::sortMode() const +{ + return m_sortMode; +} + +void FolderModel::setSortMode(int mode) +{ + if (m_sortMode != mode) { + m_sortMode = mode; + + if (mode == -1 /* Unsorted */) { + setDynamicSortFilter(false); + } else { + invalidateIfComplete(); + sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder); + setDynamicSortFilter(true); + } + + emit sortModeChanged(); + } +} + +bool FolderModel::sortDesc() const +{ + return m_sortDesc; +} + +void FolderModel::setSortDesc(bool desc) +{ + if (m_sortDesc != desc) { + m_sortDesc = desc; + + if (m_sortMode != -1 /* Unsorted */) { + invalidateIfComplete(); + sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder); + } + + emit sortDescChanged(); + } +} + +bool FolderModel::sortDirsFirst() const +{ + return m_sortDirsFirst; +} + +void FolderModel::setSortDirsFirst(bool enable) +{ + if (m_sortDirsFirst != enable) { + m_sortDirsFirst = enable; + + if (m_sortMode != -1 /* Unsorted */) { + invalidateIfComplete(); + sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder); + } + + emit sortDirsFirstChanged(); + } +} + +bool FolderModel::parseDesktopFiles() const +{ + return m_parseDesktopFiles; +} + +void FolderModel::setParseDesktopFiles(bool enable) +{ + if (m_parseDesktopFiles != enable) { + m_parseDesktopFiles = enable; + emit parseDesktopFilesChanged(); + } +} + +QObject *FolderModel::viewAdapter() const +{ + return m_viewAdapter; +} + +void FolderModel::setViewAdapter(QObject *adapter) +{ + if (m_viewAdapter != adapter) { + KAbstractViewAdapter *abstractViewAdapter = dynamic_cast(adapter); + + m_viewAdapter = abstractViewAdapter; + + if (m_viewAdapter && !m_previewGenerator) { + m_previewGenerator = new KFilePreviewGenerator(abstractViewAdapter, this); + m_previewGenerator->setPreviewShown(m_previews); + m_previewGenerator->setEnabledPlugins(m_effectivePreviewPlugins); + } + + emit viewAdapterChanged(); + } +} + +bool FolderModel::previews() const +{ + return m_previews; +} + +void FolderModel::setPreviews(bool previews) +{ + if (m_previews != previews) { + m_previews = previews; + + if (m_previewGenerator) { + m_previewGenerator->setPreviewShown(m_previews); + } + + emit previewsChanged(); + } +} + +QStringList FolderModel::previewPlugins() const +{ + return m_previewPlugins; +} + +void FolderModel::setPreviewPlugins(const QStringList &previewPlugins) +{ + QStringList effectivePlugins = previewPlugins; + if (effectivePlugins.isEmpty()) { + effectivePlugins = KIO::PreviewJob::defaultPlugins(); + } + + if (m_effectivePreviewPlugins != effectivePlugins) { + m_effectivePreviewPlugins = effectivePlugins; + + if (m_previewGenerator) { + m_previewGenerator->setPreviewShown(false); + m_previewGenerator->setEnabledPlugins(m_effectivePreviewPlugins); + m_previewGenerator->setPreviewShown(true); + } + } + + if (m_previewPlugins != previewPlugins) { + m_previewPlugins = previewPlugins; + emit previewPluginsChanged(); + } +} + +int FolderModel::filterMode() const +{ + return m_filterMode; +} + +void FolderModel::setFilterMode(int filterMode) +{ + if (m_filterMode != (FilterMode)filterMode) { + m_filterMode = (FilterMode)filterMode; + + invalidateFilterIfComplete(); + + emit filterModeChanged(); + } +} + +QString FolderModel::filterPattern() const +{ + return m_filterPattern; +} + +void FolderModel::setFilterPattern(const QString &pattern) +{ + if (m_filterPattern == pattern) { + return; + } + + m_filterPattern = pattern; + m_filterPatternMatchAll = (pattern == QLatin1String("*")); + + const QStringList patterns = pattern.split(QLatin1Char(' ')); + m_regExps.clear(); + m_regExps.reserve(patterns.count()); + + foreach (const QString &pattern, patterns) { + QRegExp rx(pattern); + rx.setPatternSyntax(QRegExp::Wildcard); + rx.setCaseSensitivity(Qt::CaseInsensitive); + m_regExps.append(rx); + } + + invalidateFilterIfComplete(); + + emit filterPatternChanged(); +} + +QStringList FolderModel::filterMimeTypes() const +{ + return m_mimeSet.values(); +} + +void FolderModel::setFilterMimeTypes(const QStringList &mimeList) +{ + const QSet set(mimeList.constBegin(), mimeList.constEnd()); + + if (m_mimeSet != set) { + m_mimeSet = set; + + invalidateFilterIfComplete(); + + emit filterMimeTypesChanged(); + } +} + +void FolderModel::setScreen(int screen) +{ + m_screenUsed = (screen != -1); + + if (!m_screenUsed || m_screen == screen) + return; + + m_screen = screen; +// if (m_usedByContainment && !m_screenMapper->sharedDesktops()) { +// m_screenMapper->addScreen(screen, resolvedUrl()); +// } + emit screenChanged(); +} + +bool FolderModel::eventFilter(QObject *watched, QEvent *event) +{ + Q_UNUSED(watched) + + // Catching Shift modifier usage on open context menus to swap the + // Trash/Delete actions. + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + + if (keyEvent->key() == Qt::Key_Shift) { + m_actionCollection.action(QStringLiteral("trash"))->setVisible(false); + m_actionCollection.action(QStringLiteral("del"))->setVisible(true); + } + } else if (event->type() == QEvent::KeyRelease) { + QKeyEvent *keyEvent = static_cast(event); + + if (keyEvent->key() == Qt::Key_Shift) { + m_actionCollection.action(QStringLiteral("trash"))->setVisible(true); + m_actionCollection.action(QStringLiteral("del"))->setVisible(false); + } + } + + return false; +} + +KFileItem FolderModel::rootItem() const +{ + return m_dirModel->dirLister()->rootItem(); +} + +void FolderModel::up() +{ + const QUrl &up = KIO::upUrl(resolvedUrl()); + + if (up.isValid()) { + setUrl(up.toString()); + } +} + +void FolderModel::cd(int row) +{ + if (row < 0) { + return; + } + + const QModelIndex idx = index(row, 0); + bool isDir = data(idx, IsDirRole).toBool(); + + if (isDir) { + const KFileItem item = itemForIndex(idx); + if (m_parseDesktopFiles && item.isDesktopFile()) { + const KDesktopFile file(item.targetUrl().path()); + if (file.hasLinkType()) { + setUrl(file.readUrl()); + } + } else { + setUrl(item.targetUrl().toString()); + } + } +} + +void FolderModel::run(int row) +{ + if (row < 0) { + return; + } + + KFileItem item = itemForIndex(index(row, 0)); + + QUrl url(item.targetUrl()); + + // FIXME TODO: This can go once we depend on a KIO w/ fe1f50caaf2. + if (url.scheme().isEmpty()) { + url.setScheme(QStringLiteral("file")); + } + + KRun *run = new KRun(url, nullptr); + // On desktop:/ we want to be able to run .desktop files right away, + // otherwise ask for security reasons. We also don't use the targetUrl() + // from above since we don't want the resolved /home/foo/Desktop URL. + run->setShowScriptExecutionPrompt(item.url().scheme() != QLatin1String("desktop") + || item.url().adjusted(QUrl::RemoveFilename).path() != QLatin1String("/")); +} + +void FolderModel::runSelected() +{ + if (!m_selectionModel->hasSelection()) { + return; + } + + if (m_selectionModel->selectedIndexes().count() == 1) { + run(m_selectionModel->selectedIndexes().constFirst().row()); + return; + } + + KFileItemActions fileItemActions(this); + KFileItemList items; + + foreach (const QModelIndex &index, m_selectionModel->selectedIndexes()) { + // Skip over directories. + if (!index.data(IsDirRole).toBool()) { + items << itemForIndex(index); + } + } + + fileItemActions.runPreferredApplications(items, QString()); +} + +void FolderModel::rename(int row, const QString &name) +{ + if (row < 0) { + return; + } + + QModelIndex idx = index(row, 0); + m_dirModel->setData(mapToSource(idx), name, Qt::EditRole); +} + +int FolderModel::fileExtensionBoundary(int row) +{ + const QModelIndex idx = index(row, 0); + const QString &name = data(idx, Qt::DisplayRole).toString(); + + int boundary = name.length(); + + if (data(idx, IsDirRole).toBool()) { + return boundary; + } + + QMimeDatabase db; + const QString &ext = db.suffixForFileName(name); + + if (ext.isEmpty()) { + boundary = name.lastIndexOf(QLatin1Char('.')); + + if (boundary < 1) { + boundary = name.length(); + } + } else { + boundary -= ext.length() + 1; + } + + return boundary; +} + +bool FolderModel::hasSelection() const +{ + return m_selectionModel->hasSelection(); +} + +bool FolderModel::isSelected(int row) +{ + if (row < 0) { + return false; + } + + return m_selectionModel->isSelected(index(row, 0)); +} + +void FolderModel::setSelected(int row) +{ + if (row < 0) { + return; + } + + m_selectionModel->select(index(row, 0), QItemSelectionModel::Select); +} + +void FolderModel::selectAll() +{ + setRangeSelected(0, rowCount() - 1); +} + +void FolderModel::toggleSelected(int row) +{ + if (row < 0) { + return; + } + + m_selectionModel->select(index(row, 0), QItemSelectionModel::Toggle); +} + +void FolderModel::setRangeSelected(int anchor, int to) +{ + if (anchor < 0 || to < 0) { + return; + } + + QItemSelection selection(index(anchor, 0), index(to, 0)); + m_selectionModel->select(selection, QItemSelectionModel::ClearAndSelect); +} + +void FolderModel::updateSelection(const QVariantList &rows, bool toggle) +{ + QItemSelection newSelection; + + int iRow = -1; + + foreach (const QVariant &row, rows) { + iRow = row.toInt(); + + if (iRow < 0) { + return; + } + + const QModelIndex &idx = index(iRow, 0); + newSelection.select(idx, idx); + } + + if (toggle) { + QItemSelection pinnedSelection = m_pinnedSelection; + pinnedSelection.merge(newSelection, QItemSelectionModel::Toggle); + m_selectionModel->select(pinnedSelection, QItemSelectionModel::ClearAndSelect); + } else { + m_selectionModel->select(newSelection, QItemSelectionModel::ClearAndSelect); + } +} + +void FolderModel::clearSelection() +{ + if (m_selectionModel->hasSelection()) { + m_selectionModel->clear(); + } +} + +void FolderModel::pinSelection() +{ + m_pinnedSelection = m_selectionModel->selection(); +} + +void FolderModel::unpinSelection() +{ + m_pinnedSelection = QItemSelection(); +} + +void FolderModel::addItemDragImage(int row, int x, int y, int width, int height, const QVariant &image) +{ + if (row < 0) { + return; + } + + delete m_dragImages.take(row); + + DragImage *dragImage = new DragImage(); + dragImage->row = row; + dragImage->rect = QRect(x, y, width, height); + dragImage->image = image.value(); + dragImage->blank = false; + + m_dragImages.insert(row, dragImage); +} + +void FolderModel::clearDragImages() +{ + qDeleteAll(m_dragImages); + m_dragImages.clear(); +} + +void FolderModel::setDragHotSpotScrollOffset(int x, int y) +{ + m_dragHotSpotScrollOffset.setX(x); + m_dragHotSpotScrollOffset.setY(y); +} + +QPoint FolderModel::dragCursorOffset(int row) +{ + DragImage *image = m_dragImages.value(row); + if (!image) { + return QPoint(0, 0); + } + + return image->cursorOffset; +} + +void FolderModel::addDragImage(QDrag *drag, int x, int y) +{ + if (!drag || m_dragImages.isEmpty()) { + return; + } + + QRegion region; + + foreach (DragImage *image, m_dragImages) { + image->blank = isBlank(image->row); + image->rect.translate(-m_dragHotSpotScrollOffset.x(), -m_dragHotSpotScrollOffset.y()); + if (!image->blank && !image->image.isNull()) { + region = region.united(image->rect); + } + } + + QRect rect = region.boundingRect(); + QPoint offset = rect.topLeft(); + rect.translate(-offset.x(), -offset.y()); + + QImage dragImage(rect.size(), QImage::Format_RGBA8888); + dragImage.fill(Qt::transparent); + + QPainter painter(&dragImage); + + QPoint pos; + + foreach (DragImage *image, m_dragImages) { + if (!image->blank && !image->image.isNull()) { + pos = image->rect.translated(-offset.x(), -offset.y()).topLeft(); + image->cursorOffset.setX(pos.x() - (x - offset.x())); + image->cursorOffset.setY(pos.y() - (y - offset.y())); + + painter.drawImage(pos, image->image); + } + + // FIXME HACK: Operate on copy. + image->rect.translate(m_dragHotSpotScrollOffset.x(), m_dragHotSpotScrollOffset.y()); + } + + drag->setPixmap(QPixmap::fromImage(dragImage)); + drag->setHotSpot(QPoint(x - offset.x(), y - offset.y())); +} + +void FolderModel::dragSelected(int x, int y) +{ + if (m_dragInProgress) { + return; + } + + m_dragInProgress = true; + emit draggingChanged(); + m_urlChangedWhileDragging = false; + + // Avoid starting a drag synchronously in a mouse handler or interferes with + // child event filtering in parent items (and thus e.g. press-and-hold hand- + // ling in a containment). + QMetaObject::invokeMethod(this, "dragSelectedInternal", Qt::QueuedConnection, Q_ARG(int, x), Q_ARG(int, y)); +} + +void FolderModel::dragSelectedInternal(int x, int y) +{ + if (!m_viewAdapter || !m_selectionModel->hasSelection()) { + m_dragInProgress = false; + emit draggingChanged(); + return; + } + + ItemViewAdapter *adapter = qobject_cast(m_viewAdapter); + QQuickItem *item = qobject_cast(adapter->adapterView()); + + QDrag *drag = new QDrag(item); + + addDragImage(drag, x, y); + + m_dragIndexes = m_selectionModel->selectedIndexes(); + + std::sort(m_dragIndexes.begin(), m_dragIndexes.end()); + + // TODO: Optimize to emit contiguous groups. + emit dataChanged(m_dragIndexes.first(), m_dragIndexes.last(), QVector() << BlankRole); + + QModelIndexList sourceDragIndexes; + sourceDragIndexes.reserve(m_dragIndexes.count()); + foreach (const QModelIndex &index, m_dragIndexes) { + sourceDragIndexes.append(mapToSource(index)); + } + + drag->setMimeData(m_dirModel->mimeData(sourceDragIndexes)); + + // Due to spring-loading (aka auto-expand), the URL might change + // while the drag is in-flight - in that case we don't want to + // unnecessarily emit dataChanged() for (possibly invalid) indices + // after it ends. + const QUrl currentUrl(m_dirModel->dirLister()->url()); + + item->grabMouse(); + drag->exec(supportedDragActions()); + + item->ungrabMouse(); + + m_dragInProgress = false; + emit draggingChanged(); + m_urlChangedWhileDragging = false; + + if (m_dirModel->dirLister()->url() == currentUrl) { + const QModelIndex first(m_dragIndexes.first()); + const QModelIndex last(m_dragIndexes.last()); + m_dragIndexes.clear(); + // TODO: Optimize to emit contiguous groups. + emit dataChanged(first, last, QVector() << BlankRole); + } +} + +static bool isDropBetweenSharedViews(const QList &urls, const QUrl &folderUrl) +{ + for (const auto &url : urls) { + if (folderUrl.adjusted(QUrl::StripTrailingSlash) != url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)) { + return false; + } + } + return true; +} + +void FolderModel::drop(QQuickItem *target, QObject *dropEvent, int row, bool showMenuManually) +{ + QMimeData *mimeData = qobject_cast(dropEvent->property("mimeData").value()); + + if (!mimeData) { + return; + } + + QModelIndex idx; + KFileItem item; + + if (row > -1 && row < rowCount()) { + idx = index(row, 0); + item = itemForIndex(idx); + } + + QUrl dropTargetUrl; + + // So we get to run mostLocalUrl() over the current URL. + if (item.isNull()) { + item = rootItem(); + } + + if (item.isNull()) { + dropTargetUrl = m_dirModel->dirLister()->url(); + } else if (m_parseDesktopFiles && item.isDesktopFile()) { + const KDesktopFile file(item.targetUrl().path()); + + if (file.hasLinkType()) { + dropTargetUrl = QUrl(file.readUrl()); + } else { + dropTargetUrl = item.mostLocalUrl(); + } + } else { + dropTargetUrl = item.mostLocalUrl(); + } + + auto dropTargetFolderUrl = dropTargetUrl; + if (dropTargetFolderUrl.fileName() == QLatin1Char('.')) { + // the target URL for desktop:/ is e.g. 'file://home/user/Desktop/.' + dropTargetFolderUrl = dropTargetFolderUrl.adjusted(QUrl::RemoveFilename); + } + + // use dropTargetUrl to resolve desktop:/ to the actual file location which is also used by the mime data + /* QMimeData operates on local URLs, but the dir lister and thus screen mapper and positioner may + * use a fancy scheme like desktop:/ instead. Ensure we always use the latter to properly map URLs, + * i.e. go from file:///home/user/Desktop/file to desktop:/file + */ +// auto mappableUrl = [this, dropTargetFolderUrl](const QUrl &url) -> QUrl { +// if (dropTargetFolderUrl != m_dirModel->dirLister()->url()) { +// QString mappedUrl = url.toString(); +// const auto local = dropTargetFolderUrl.toString(); +// const auto internal = m_dirModel->dirLister()->url().toString(); +// if (mappedUrl.startsWith(local)) { +// mappedUrl.replace(0, local.size(), internal); +// } +// return ScreenMapper::stringToUrl(mappedUrl); +// } +// return url; +// }; + + const int x = dropEvent->property("x").toInt(); + const int y = dropEvent->property("y").toInt(); + const QPoint dropPos = {x, y}; + + if (m_dragInProgress && row == -1 && !m_urlChangedWhileDragging) { + if (m_locked || mimeData->urls().isEmpty()) { + return; + } + + setSortMode(-1); + + for (const auto &url : mimeData->urls()) { + m_dropTargetPositions.insert(url.fileName(), dropPos); +// m_screenMapper->addMapping(mappableUrl(url), m_screen, ScreenMapper::DelayedSignal); +// m_screenMapper->removeItemFromDisabledScreen(mappableUrl(url)); + } + emit move(x, y, mimeData->urls()); + + return; + } + + if (mimeData->hasFormat(QStringLiteral("application/x-kde-ark-dndextract-service")) + && mimeData->hasFormat(QStringLiteral("application/x-kde-ark-dndextract-path"))) { + const QString remoteDBusClient = mimeData->data(QStringLiteral("application/x-kde-ark-dndextract-service")); + const QString remoteDBusPath = mimeData->data(QStringLiteral("application/x-kde-ark-dndextract-path")); + + QDBusMessage message = QDBusMessage::createMethodCall(remoteDBusClient, + remoteDBusPath, + QStringLiteral("org.kde.ark.DndExtract"), + QStringLiteral("extractSelectedFilesTo")); + message.setArguments({dropTargetUrl.toDisplayString(QUrl::PreferLocalFile)}); + + QDBusConnection::sessionBus().call(message, QDBus::NoBlock); + + return; + } + + if (idx.isValid() && !(flags(idx) & Qt::ItemIsDropEnabled)) { + return; + } + + // Catch drops from a Task Manager and convert to usable URL. + if (!mimeData->hasUrls() && mimeData->hasFormat(QStringLiteral("text/x-orgkdeplasmataskmanager_taskurl"))) { + QList urls = {QUrl(QString::fromUtf8(mimeData->data(QStringLiteral("text/x-orgkdeplasmataskmanager_taskurl"))))}; + mimeData->setUrls(urls); + } + + if (m_usedByContainment /*&& !m_screenMapper->sharedDesktops()*/) { + if (isDropBetweenSharedViews(mimeData->urls(), dropTargetFolderUrl)) { + setSortMode(-1); + const QList urls = mimeData->urls(); + for (const auto &url : urls) { + m_dropTargetPositions.insert(url.fileName(), dropPos); +// m_screenMapper->addMapping(mappableUrl(url), m_screen, ScreenMapper::DelayedSignal); +// m_screenMapper->removeItemFromDisabledScreen(mappableUrl(url)); + } + m_dropTargetPositionsCleanup->start(); + return; + } + } + + Qt::DropAction proposedAction((Qt::DropAction)dropEvent->property("proposedAction").toInt()); + Qt::DropActions possibleActions(dropEvent->property("possibleActions").toInt()); + Qt::MouseButtons buttons(dropEvent->property("buttons").toInt()); + Qt::KeyboardModifiers modifiers(dropEvent->property("modifiers").toInt()); + + auto pos = target->mapToScene(dropPos).toPoint(); + pos = target->window()->mapToGlobal(pos); + QDropEvent ev(pos, possibleActions, mimeData, buttons, modifiers); + ev.setDropAction(proposedAction); + + KIO::DropJobFlag flag = showMenuManually ? KIO::ShowMenuManually : KIO::DropJobDefaultFlags; + KIO::DropJob *dropJob = KIO::drop(&ev, dropTargetUrl, flag); + dropJob->uiDelegate()->setAutoErrorHandlingEnabled(true); + + // The QMimeData we extract from the DropArea's drop event is deleted as soon as this method + // ends but we need to keep a copy for when popupMenuAboutToShow fires. + QMimeData *mimeCopy = new QMimeData(); + const QStringList formats = mimeData->formats(); + for (const QString &format : formats) { + mimeCopy->setData(format, mimeData->data(format)); + } + + connect(dropJob, &KIO::DropJob::popupMenuAboutToShow, this, [this, mimeCopy, x, y, dropJob](const KFileItemListProperties &) { + emit popupMenuAboutToShow(dropJob, mimeCopy, x, y); + mimeCopy->deleteLater(); + }); + + /* + * Position files that come from a drag'n'drop event at the drop event + * target position. To do so, we first listen to copy job to figure out + * the target URL. Then we store the position of this drop event in the + * hash and eventually trigger a move request when we get notified about + * the new file event from the source model. + */ + connect(dropJob, &KIO::DropJob::copyJobStarted, this, [this, dropPos, dropTargetUrl](KIO::CopyJob *copyJob) { + auto map = [this, dropPos, dropTargetUrl](const QUrl &targetUrl) { + m_dropTargetPositions.insert(targetUrl.fileName(), dropPos); + m_dropTargetPositionsCleanup->start(); + + if (m_usedByContainment /*&& !m_screenMapper->sharedDesktops()*/) { + // assign a screen for the item before the copy is actually done, so + // filterAcceptsRow doesn't assign the default screen to it + QUrl url = resolvedUrl(); + // if the folderview's folder is a standard path, just use the targetUrl for mapping + if (targetUrl.toString().startsWith(url.toString())) { +// m_screenMapper->addMapping(targetUrl, m_screen, ScreenMapper::DelayedSignal); + } else if (targetUrl.toString().startsWith(dropTargetUrl.toString())) { + // if the folderview's folder is a special path, like desktop:// , we need to convert + // the targetUrl file:// path to a desktop:/ path for mapping + auto destPath = dropTargetUrl.path(); + auto filePath = targetUrl.path(); + if (filePath.startsWith(destPath)) { + url.setPath(filePath.remove(0, destPath.length())); +// m_screenMapper->addMapping(url, m_screen, ScreenMapper::DelayedSignal); + } + } + } + }; + // remember drop target position for target URL and forget about the source URL + connect(copyJob, &KIO::CopyJob::copyingDone, this, [map](KIO::Job *, const QUrl &, const QUrl &targetUrl, const QDateTime &, bool, bool) { + map(targetUrl); + }); + connect(copyJob, &KIO::CopyJob::copyingLinkDone, this, [map](KIO::Job *, const QUrl &, const QString &, const QUrl &targetUrl) { + map(targetUrl); + }); + }); +} + +void FolderModel::dropCwd(QObject *dropEvent) +{ + QMimeData *mimeData = qobject_cast(dropEvent->property("mimeData").value()); + + if (!mimeData) { + return; + } + + if (mimeData->hasFormat(QStringLiteral("application/x-kde-ark-dndextract-service")) + && mimeData->hasFormat(QStringLiteral("application/x-kde-ark-dndextract-path"))) { + const QString remoteDBusClient = mimeData->data(QStringLiteral("application/x-kde-ark-dndextract-service")); + const QString remoteDBusPath = mimeData->data(QStringLiteral("application/x-kde-ark-dndextract-path")); + + QDBusMessage message = QDBusMessage::createMethodCall(remoteDBusClient, + remoteDBusPath, + QStringLiteral("org.kde.ark.DndExtract"), + QStringLiteral("extractSelectedFilesTo")); + message.setArguments(QVariantList() << m_dirModel->dirLister()->url().adjusted(QUrl::PreferLocalFile).toString()); + + QDBusConnection::sessionBus().call(message, QDBus::NoBlock); + } else { + Qt::DropAction proposedAction((Qt::DropAction)dropEvent->property("proposedAction").toInt()); + Qt::DropActions possibleActions(dropEvent->property("possibleActions").toInt()); + Qt::MouseButtons buttons(dropEvent->property("buttons").toInt()); + Qt::KeyboardModifiers modifiers(dropEvent->property("modifiers").toInt()); + + QDropEvent ev(QPoint(), possibleActions, mimeData, buttons, modifiers); + ev.setDropAction(proposedAction); + + KIO::DropJob *dropJob = KIO::drop(&ev, m_dirModel->dirLister()->url().adjusted(QUrl::PreferLocalFile)); + dropJob->uiDelegate()->setAutoErrorHandlingEnabled(true); + } +} + +void FolderModel::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + QModelIndexList indices = selected.indexes(); + indices.append(deselected.indexes()); + + QVector roles; + roles.append(SelectedRole); + + foreach (const QModelIndex &index, indices) { + emit dataChanged(index, index, roles); + } + + if (!m_selectionModel->hasSelection()) { + clearDragImages(); + } else { + foreach (const QModelIndex &idx, deselected.indexes()) { + delete m_dragImages.take(idx.row()); + } + } + + updateActions(); +} + +bool FolderModel::isBlank(int row) const +{ + if (row < 0) { + return true; + } + + return data(index(row, 0), BlankRole).toBool(); +} + +QVariant FolderModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (role == BlankRole) { + return m_dragIndexes.contains(index); + } else if (role == OverlaysRole) { + const KFileItem item = itemForIndex(index); + return item.overlays(); + } else if (role == SelectedRole) { + return m_selectionModel->isSelected(index); + } else if (role == IsDirRole) { + return isDir(mapToSource(index), m_dirModel); + } else if (role == IsLinkRole) { + const KFileItem item = itemForIndex(index); + return item.isLink(); + } else if (role == IsHiddenRole) { + const KFileItem item = itemForIndex(index); + return item.isHidden(); + } else if (role == UrlRole) { + return itemForIndex(index).url(); + } else if (role == LinkDestinationUrl) { + const KFileItem item = itemForIndex(index); + + if (m_parseDesktopFiles && item.isDesktopFile()) { + const KDesktopFile file(item.targetUrl().path()); + + if (file.hasLinkType()) { + return file.readUrl(); + } + } + + return item.targetUrl(); + } else if (role == SizeRole) { + bool isDir = data(index, IsDirRole).toBool(); + + if (!isDir) { + return m_dirModel->data(mapToSource(QSortFilterProxyModel::index(index.row(), 1)), Qt::DisplayRole); + } + } else if (role == TypeRole) { + return m_dirModel->data(mapToSource(QSortFilterProxyModel::index(index.row(), 6)), Qt::DisplayRole); + } else if (role == FileNameRole) { + return itemForIndex(index).url().fileName(); + } + + return QSortFilterProxyModel::data(index, role); +} + +int FolderModel::indexForUrl(const QUrl &url) const +{ + return mapFromSource(m_dirModel->indexForUrl(url)).row(); +} + +KFileItem FolderModel::itemForIndex(const QModelIndex &index) const +{ + return m_dirModel->itemForIndex(mapToSource(index)); +} + +bool FolderModel::isDir(const QModelIndex &index, const KDirModel *dirModel) const +{ + KFileItem item = dirModel->itemForIndex(index); + if (item.isDir()) { + return true; + } + + auto it = m_isDirCache.constFind(item.url()); + if (it != m_isDirCache.constEnd()) { + return *it; + } + + if (m_parseDesktopFiles && item.isDesktopFile()) { + // Check if the desktop file is a link to a directory + KDesktopFile file(item.targetUrl().path()); + + if (!file.hasLinkType()) { + return false; + } + + const QUrl url(file.readUrl()); + + // Check if we already have a running StatJob for this URL. + if (m_isDirJobs.contains(item.url())) { + return false; + } + + // Assume the root folder of a protocol is always a folder. + // This avoids spinning up e.g. trash KIO slave just to check whether trash:/ is a folder. + if (url.path() == QLatin1String("/")) { + m_isDirCache.insert(item.url(), true); + return true; + } + + if (KProtocolInfo::protocolClass(url.scheme()) != QLatin1String(":local")) { + return false; + } + + KIO::StatJob *job = KIO::stat(url, KIO::HideProgressInfo); + job->setProperty("org.kde.plasma.folder_url", item.url()); + job->setSide(KIO::StatJob::SourceSide); + job->setDetails(0); + connect(job, &KJob::result, this, &FolderModel::statResult); + m_isDirJobs.insert(item.url(), job); + } + + return false; +} + +void FolderModel::statResult(KJob *job) +{ + KIO::StatJob *statJob = static_cast(job); + + const QUrl &url = statJob->property("org.kde.plasma.folder_url").toUrl(); + const QModelIndex &idx = index(indexForUrl(url), 0); + + if (idx.isValid() && statJob->error() == KJob::NoError) { + m_isDirCache[url] = statJob->statResult().isDir(); + + emit dataChanged(idx, idx, QVector() << IsDirRole); + } + + m_isDirJobs.remove(url); +} + +void FolderModel::evictFromIsDirCache(const KFileItemList &items) +{ + foreach (const KFileItem &item, items) { +// m_screenMapper->removeFromMap(item.url()); + m_isDirCache.remove(item.url()); + } +} + +bool FolderModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + const KDirModel *dirModel = static_cast(sourceModel()); + + if (m_sortDirsFirst || left.column() == KDirModel::Size) { + bool leftIsDir = isDir(left, dirModel); + bool rightIsDir = isDir(right, dirModel); + + if (leftIsDir && !rightIsDir) { + return (sortOrder() == Qt::AscendingOrder); + } + + if (!leftIsDir && rightIsDir) { + return (sortOrder() == Qt::DescendingOrder); + } + } + + const KFileItem leftItem = dirModel->data(left, KDirModel::FileItemRole).value(); + const KFileItem rightItem = dirModel->data(right, KDirModel::FileItemRole).value(); + const int column = left.column(); + int result = 0; + + switch (column) { + case KDirModel::Size: { + if (isDir(left, dirModel) && isDir(right, dirModel)) { + const int leftChildCount = dirModel->data(left, KDirModel::ChildCountRole).toInt(); + const int rightChildCount = dirModel->data(right, KDirModel::ChildCountRole).toInt(); + if (leftChildCount < rightChildCount) + result = -1; + else if (leftChildCount > rightChildCount) + result = +1; + } else { + const KIO::filesize_t leftSize = leftItem.size(); + const KIO::filesize_t rightSize = rightItem.size(); + if (leftSize < rightSize) + result = -1; + else if (leftSize > rightSize) + result = +1; + } + + break; + } + case KDirModel::ModifiedTime: { + const long long leftTime = leftItem.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); + const long long rightTime = rightItem.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); + if (leftTime < rightTime) + result = -1; + else if (leftTime > rightTime) + result = +1; + + break; + } + case KDirModel::Type: + result = QString::compare(dirModel->data(left, Qt::DisplayRole).toString(), dirModel->data(right, Qt::DisplayRole).toString()); + break; + + default: + break; + } + + if (result != 0) + return result < 0; + + QCollator collator; + + result = collator.compare(leftItem.text(), rightItem.text()); + + if (result != 0) + return result < 0; + + result = collator.compare(leftItem.name(), rightItem.name()); + + if (result != 0) + return result < 0; + + return QString::compare(leftItem.url().url(), rightItem.url().url(), Qt::CaseSensitive); +} + +Qt::DropActions FolderModel::supportedDragActions() const +{ + return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction; +} + +Qt::DropActions FolderModel::supportedDropActions() const +{ + return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction; +} + +inline bool FolderModel::matchMimeType(const KFileItem &item) const +{ + if (m_mimeSet.isEmpty()) { + return false; + } + + if (m_mimeSet.contains(QLatin1String("all/all")) || m_mimeSet.contains(QLatin1String("all/allfiles"))) { + return true; + } + + const QString mimeType = item.determineMimeType().name(); + return m_mimeSet.contains(mimeType); +} + +inline bool FolderModel::matchPattern(const KFileItem &item) const +{ + if (m_filterPatternMatchAll) { + return true; + } + + const QString name = item.name(); + QListIterator i(m_regExps); + while (i.hasNext()) { + if (i.next().exactMatch(name)) { + return true; + } + } + + return false; +} + +bool FolderModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + const KDirModel *dirModel = static_cast(sourceModel()); + const KFileItem item = dirModel->itemForIndex(dirModel->index(sourceRow, KDirModel::Name, sourceParent)); + +// if (m_usedByContainment && !m_screenMapper->sharedDesktops()) { +// const QUrl url = item.url(); +// const int screen = m_screenMapper->screenForItem(url); +// // don't do anything if the folderview is not associated with a screen +// if (m_screenUsed && screen == -1) { +// // The item is not associated with a screen, probably because this is the first +// // time we see it or the folderview was previously used as a regular applet. +// // Associated with this folderview if the view is on the first available screen +// if (m_screen == m_screenMapper->firstAvailableScreen(resolvedUrl())) { +// m_screenMapper->addMapping(url, m_screen, ScreenMapper::DelayedSignal); +// } else { +// return false; +// } +// } else if (m_screen != screen) { +// // the item belongs to a different screen, filter it out +// return false; +// } +// } + + if (m_filterMode == NoFilter) { + return true; + } + + if (m_filterMode == FilterShowMatches) { + return (matchPattern(item) && matchMimeType(item)); + } else { + return !(matchPattern(item) && matchMimeType(item)); + } +} + +void FolderModel::createActions() +{ + KIO::FileUndoManager *manager = KIO::FileUndoManager::self(); + + QAction *cut = new QAction(tr("Cut"), this); + connect(cut, &QAction::triggered, this, &FolderModel::cut); + + QAction *copy = new QAction(tr("Copy"), this); + connect(copy, &QAction::triggered, this, &FolderModel::copy); + + QAction *undo = new QAction(tr("Undo"), this); + undo->setEnabled(manager->undoAvailable()); + connect(undo, &QAction::triggered, manager, &KIO::FileUndoManager::undo); + + connect(manager, SIGNAL(undoAvailable(bool)), undo, SLOT(setEnabled(bool))); + // revenmartin: not needed for now. + // connect(manager, &KIO::FileUndoManager::undoTextChanged, this, &FolderModel::undoTextChanged); + + QAction *paste = new QAction(tr("Paste"), this); + connect(paste, &QAction::triggered, this, &FolderModel::paste); + + // QAction *refresh = new QAction(tr("&Refresh View"), this); + // refresh->setShortcut(QKeySequence(QKeySequence::Refresh)); + // connect(refresh, &QAction::triggered, this, &FolderModel::refresh); + + QAction *newFolderAction = new QAction(tr("New Folder")); + connect(newFolderAction, &QAction::triggered, this, &FolderModel::createFolder); + + QAction *newDocAction = new QAction(tr("New Documents")); + connect(newDocAction, &QAction::triggered, this, [=] { + m_newMenu->setPopupFiles(QList() << m_dirModel->dirLister()->url()); + m_newMenu->createFile(); + }); + + QAction *rename = new QAction(tr("Rename"), this); + connect(rename, &QAction::triggered, this, &FolderModel::requestRename); + + QAction *trash = new QAction(tr("Move To Trash"), this); + connect(trash, &QAction::triggered, this, &FolderModel::moveSelectedToTrash); + + QAction *emptyTrash = new QAction(tr("&Empty Trash"), this); + connect(emptyTrash, &QAction::triggered, this, &FolderModel::emptyTrashBin); + + QAction *restoreFromTrash = new QAction(tr("Restore from trash"), this); + connect(restoreFromTrash, &QAction::triggered, this, &FolderModel::restoreSelectedFromTrash); + + QAction *del = new QAction(tr("Delete"), this); + connect(del, &QAction::triggered, this, &FolderModel::deleteSelected); + + QAction *actOpen = new QAction(tr("&Open"), this); + connect(actOpen, &QAction::triggered, this, &FolderModel::openSelected); + + m_actionCollection.addAction(QStringLiteral("open"), actOpen); + m_actionCollection.addAction(QStringLiteral("cut"), cut); + m_actionCollection.addAction(QStringLiteral("undo"), undo); + m_actionCollection.addAction(QStringLiteral("copy"), copy); + m_actionCollection.addAction(QStringLiteral("paste"), paste); + // m_actionCollection.addAction(QStringLiteral("refresh"), refresh); + m_actionCollection.addAction(QStringLiteral("rename"), rename); + m_actionCollection.addAction(QStringLiteral("trash"), trash); + m_actionCollection.addAction(QStringLiteral("del"), del); + m_actionCollection.addAction(QStringLiteral("restoreFromTrash"), restoreFromTrash); + m_actionCollection.addAction(QStringLiteral("emptyTrash"), emptyTrash); + m_actionCollection.addAction(QStringLiteral("new_folder"), newFolderAction); + m_actionCollection.addAction(QStringLiteral("new_documents"), newDocAction); + + m_newMenu = new KNewFileMenu(&m_actionCollection, QStringLiteral("newMenu"), this); + m_newMenu->setModal(false); + connect(m_newMenu, &KNewFileMenu::directoryCreated, this, &FolderModel::newFileMenuItemCreated); + connect(m_newMenu, &KNewFileMenu::fileCreated, this, &FolderModel::newFileMenuItemCreated); + + m_copyToMenu = new KFileCopyToMenu(nullptr); +} + +QAction *FolderModel::action(const QString &name) const +{ + return m_actionCollection.action(name); +} + +QObject *FolderModel::newMenu() const +{ + return m_newMenu->menu(); +} + +void FolderModel::updateActions() +{ + const QModelIndexList indexes = m_selectionModel->selectedIndexes(); + + KFileItemList items; + QList urls; + bool hasRemoteFiles = false; + bool isTrashLink = false; + const bool isTrash = (resolvedUrl().scheme() == QLatin1String("trash")); + + if (indexes.isEmpty()) { + items << rootItem(); + } else { + items.reserve(indexes.count()); + urls.reserve(indexes.count()); + for (const QModelIndex &index : indexes) { + KFileItem item = itemForIndex(index); + if (!item.isNull()) { + hasRemoteFiles |= item.localPath().isEmpty(); + items.append(item); + urls.append(item.url()); + } + } + } + + KFileItemListProperties itemProperties(items); + // Check if we're showing the menu for the trash link + if (items.count() == 1 && items.at(0).isDesktopFile()) { + KDesktopFile file(items.at(0).localPath()); + if (file.hasLinkType() && file.readUrl() == QLatin1String("trash:/")) { + isTrashLink = true; + } + } + + if (m_newMenu) { + m_newMenu->checkUpToDate(); + m_newMenu->setPopupFiles(QList() << m_dirModel->dirLister()->url()); + // we need to set here as well, when the menu is shown via AppletInterface::eventFilter + m_menuPosition = QCursor::pos(); + + if (QAction *newMenuAction = m_actionCollection.action(QStringLiteral("newMenu"))) { + newMenuAction->setEnabled(itemProperties.supportsWriting()); + newMenuAction->setVisible(!isTrash); + } + } + + if (QAction *emptyTrash = m_actionCollection.action(QStringLiteral("emptyTrash"))) { + if (isTrash || isTrashLink) { + emptyTrash->setVisible(true); + emptyTrash->setEnabled(!isTrashEmpty()); + } else { + emptyTrash->setVisible(false); + } + } + + if (QAction *newFolder = m_actionCollection.action(QStringLiteral("new_folder"))) { + newFolder->setVisible(!isTrash); + } + + if (QAction *newDocuments = m_actionCollection.action(QStringLiteral("new_documents"))) { + newDocuments->setVisible(!isTrash); + } + + if (QAction *restoreFromTrash = m_actionCollection.action(QStringLiteral("restoreFromTrash"))) { + restoreFromTrash->setVisible(isTrash); + } + + if (QAction *moveToTrash = m_actionCollection.action(QStringLiteral("trash"))) { + moveToTrash->setVisible(!hasRemoteFiles && itemProperties.supportsMoving()); + } + + if (QAction *del = m_actionCollection.action(QStringLiteral("del"))) { + del->setVisible(itemProperties.supportsDeleting()); + } + + if (QAction *cut = m_actionCollection.action(QStringLiteral("cut"))) { + cut->setEnabled(itemProperties.supportsDeleting()); + cut->setVisible(!isTrash); + } + + if (QAction *paste = m_actionCollection.action(QStringLiteral("paste"))) { + bool enable = false; + + QList urls = KUrlMimeData::urlsFromMimeData(QApplication::clipboard()->mimeData()); + + if (!urls.isEmpty() && rootItem().isWritable()) { + enable = rootItem().isWritable(); + } + + paste->setEnabled(enable); + + } + + if (QAction *rename = m_actionCollection.action(QStringLiteral("rename"))) { + rename->setEnabled(itemProperties.supportsMoving()); + rename->setVisible(!isTrash); + } +} + +void FolderModel::openContextMenu(QQuickItem *visualParent, Qt::KeyboardModifiers modifiers) +{ + if (m_usedByContainment && !KAuthorized::authorize(QStringLiteral("action/kdesktop_rmb"))) { + return; + } + + updateActions(); + + const QModelIndexList indexes = m_selectionModel->selectedIndexes(); + QMenu *menu = new QMenu(); + + if (!m_fileItemActions) { + m_fileItemActions = new KFileItemActions(this); + m_fileItemActions->setParentWidget(QApplication::desktop()); + } + + if (indexes.isEmpty()) { + QAction *selectAll = new QAction(tr("Select All"), this); + connect(selectAll, &QAction::triggered, this, &FolderModel::selectAll); + + if (KFileItem(url()).isWritable()) + menu->addAction(m_actionCollection.action(QStringLiteral("new_folder"))); + + if (KFileItem(url()).isWritable()) + menu->addAction(m_actionCollection.action(QStringLiteral("new_documents"))); + + // menu->addAction(m_actionCollection.action(QStringLiteral("newMenu"))); + menu->addSeparator(); + menu->addAction(m_actionCollection.action(QStringLiteral("paste"))); + menu->addAction(m_actionCollection.action(QStringLiteral("undo"))); + menu->addAction(selectAll); + menu->addAction(m_actionCollection.action(QStringLiteral("emptyTrash"))); + + if (m_isDesktopView) { + QAction *setWallpaper = new QAction(tr("Change Wallpaper"), this); + connect(setWallpaper, &QAction::triggered, this, [=] { openSettings("wallpaper"); }); + menu->addSeparator(); + menu->addAction(setWallpaper); + } + + // Properties + if (KPropertiesDialog::canDisplay(KFileItemList() << rootItem())) { + menu->addSeparator(); + QAction *act = new QAction(tr("Properties"), menu); + QObject::connect(act, &QAction::triggered, this, &FolderModel::openPropertiesDialog); + menu->addAction(act); + } + } else { + KFileItemList items; + QList urls; + + items.reserve(indexes.count()); + urls.reserve(indexes.count()); + for (const QModelIndex &index : indexes) { + KFileItem item = itemForIndex(index); + if (!item.isNull()) { + items.append(item); + urls.append(item.url()); + } + } + + KFileItemListProperties itemProperties(items); + + // Start adding the actions: + // "Open" and "Open with" actions + m_fileItemActions->setItemListProperties(itemProperties); + m_fileItemActions->addOpenWithActionsTo(menu); + + menu->addSeparator(); + menu->addAction(m_actionCollection.action(QStringLiteral("cut"))); + menu->addAction(m_actionCollection.action(QStringLiteral("copy"))); + + menu->addAction(m_actionCollection.action(QStringLiteral("rename"))); + menu->addSeparator(); + menu->addAction(m_actionCollection.action(QStringLiteral("restoreFromTrash"))); + + menu->addAction(m_actionCollection.action(QStringLiteral("emptyTrash"))); + + QAction *trashAction = m_actionCollection.action(QStringLiteral("trash")); + menu->addAction(trashAction); + trashAction->setVisible(!modifiers.testFlag(Qt::ShiftModifier)); + + QAction *deleteAction = m_actionCollection.action(QStringLiteral("del")); + menu->addAction(deleteAction); + + deleteAction->setVisible(!trashAction->isVisible()); + + menu->addSeparator(); + + // Set as Wallpaper + if (indexes.count() == 1 && items.first().mimetype().startsWith("image/")) { + QAction *setAswallpaper = new QAction(tr("Set as Wallpaper"), this); + connect(setAswallpaper, &QAction::triggered, this, &FolderModel::setAsWallpaper); + menu->addAction(setAswallpaper); + } + + // Properties + if (KPropertiesDialog::canDisplay(items)) { + menu->addSeparator(); + QAction *act = new QAction(tr("&Properties"), menu); + QObject::connect(act, &QAction::triggered, this, &FolderModel::openPropertiesDialog); + menu->addAction(act); + } + } + + if (visualParent) { + m_menuPosition = visualParent->mapToGlobal(QPointF(0, visualParent->height())).toPoint(); + } else { + m_menuPosition = QCursor::pos(); + } + + // Used to monitor Shift modifier usage while the menu is open, to + // swap the Trash and Delete actions. + menu->installEventFilter(this); + + menu->setAttribute(Qt::WA_TranslucentBackground); + menu->winId(); // force surface creation before ensurePolish call in menu::Popup which happens before show + menu->popup(m_menuPosition); + connect(menu, &QMenu::aboutToHide, [menu]() { + menu->deleteLater(); + }); +} + +void FolderModel::openPropertiesDialog() +{ + const QModelIndexList indexes = m_selectionModel->selectedIndexes(); + + if (indexes.isEmpty()) { + PropertiesDialog::showDialog(QUrl::fromLocalFile(url())); + return; + } + + KFileItemList items; + items.reserve(indexes.count()); + for (const QModelIndex &index : indexes) { + KFileItem item = itemForIndex(index); + if (!item.isNull()) { + items.append(item); + } + } + + PropertiesDialog::showDialog(items); +} + +QString FolderModel::desktopPath() const +{ + return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); +} + +QString FolderModel::homePath() const +{ + return QStandardPaths::writableLocation(QStandardPaths::HomeLocation); +} + +void FolderModel::linkHere(const QUrl &sourceUrl) +{ + KIO::CopyJob *job = KIO::link(sourceUrl, m_dirModel->dirLister()->url(), KIO::HideProgressInfo); + KIO::FileUndoManager::self()->recordCopyJob(job); +} + +QList FolderModel::selectedUrls() const +{ + const auto indexes = m_selectionModel->selectedIndexes(); + + QList urls; + urls.reserve(indexes.count()); + + for (const QModelIndex &index : indexes) { + urls.append(itemForIndex(index).url()); + } + + return urls; +} + +void FolderModel::copy() +{ + if (!m_selectionModel->hasSelection()) { + return; + } + + if (QAction *action = m_actionCollection.action(QStringLiteral("copy"))) { + if (!action->isEnabled()) { + return; + } + } + + QMimeData *mimeData = QSortFilterProxyModel::mimeData(m_selectionModel->selectedIndexes()); + QApplication::clipboard()->setMimeData(mimeData); +} + +void FolderModel::cut() +{ + if (!m_selectionModel->hasSelection()) { + return; + } + + if (QAction *action = m_actionCollection.action(QStringLiteral("cut"))) { + if (!action->isEnabled()) { + return; + } + } + + QMimeData *mimeData = QSortFilterProxyModel::mimeData(m_selectionModel->selectedIndexes()); + KIO::setClipboardDataCut(mimeData, true); + QApplication::clipboard()->setMimeData(mimeData); +} + +void FolderModel::paste() +{ + if (QAction *action = m_actionCollection.action(QStringLiteral("paste"))) { + if (!action->isEnabled()) { + return; + } + } + + KIO::paste(QApplication::clipboard()->mimeData(), m_dirModel->dirLister()->url()); +} + +void FolderModel::pasteTo() +{ + const QList urls = selectedUrls(); + Q_ASSERT(urls.count() == 1); + KIO::paste(QApplication::clipboard()->mimeData(), urls.first()); +} + +void FolderModel::refresh() +{ + m_errorString.clear(); + emit errorStringChanged(); + + m_dirModel->dirLister()->updateDirectory(m_dirModel->dirLister()->url()); +} + +void FolderModel::moveSelectedToTrash() +{ + if (!m_selectionModel->hasSelection()) { + return; + } + + if (QAction *action = m_actionCollection.action(QStringLiteral("trash"))) { + if (!action->isEnabled()) { + return; + } + } + + const QList urls = selectedUrls(); + KIO::JobUiDelegate uiDelegate; + + if (uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Trash, KIO::JobUiDelegate::DefaultConfirmation)) { + KIO::Job *job = KIO::trash(urls); + job->uiDelegate()->setAutoErrorHandlingEnabled(true); + KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Trash, urls, QUrl(QStringLiteral("trash:/")), job); + } +} + +void FolderModel::deleteSelected() +{ + if (!m_selectionModel->hasSelection()) { + return; + } + + if (QAction *action = m_actionCollection.action(QStringLiteral("del"))) { + if (!action->isEnabled()) { + return; + } + } + + const QList urls = selectedUrls(); + KIO::JobUiDelegate uiDelegate; + + if (uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Delete, KIO::JobUiDelegate::DefaultConfirmation)) { + KIO::Job *job = KIO::del(urls); + job->uiDelegate()->setAutoErrorHandlingEnabled(true); + } +} + +void FolderModel::openSelected() +{ + if (!m_selectionModel->hasSelection()) { + return; + } + + const QList urls = selectedUrls(); + for (const QUrl &url : urls) { + (void)new KRun(url, nullptr); + } +} + +void FolderModel::undo() +{ + if (QAction *action = m_actionCollection.action(QStringLiteral("undo"))) { + // trigger() doesn't check enabled and would crash if invoked nonetheless. + if (action->isEnabled()) { + action->trigger(); + } + } +} + +void FolderModel::emptyTrashBin() +{ + KIO::JobUiDelegate uiDelegate; + uiDelegate.setWindow(QApplication::desktop()); + + if (uiDelegate.askDeleteConfirmation(QList(), KIO::JobUiDelegate::EmptyTrash, KIO::JobUiDelegate::DefaultConfirmation)) { + KIO::Job *job = KIO::emptyTrash(); + job->uiDelegate()->setAutoErrorHandlingEnabled(true); + } +} + +void FolderModel::restoreSelectedFromTrash() +{ + if (!m_selectionModel->hasSelection()) { + return; + } + + const auto &urls = selectedUrls(); + + KIO::RestoreJob *job = KIO::restoreFromTrash(urls); + job->uiDelegate()->setAutoErrorHandlingEnabled(true); +} + +bool FolderModel::isTrashEmpty() +{ + KConfig trashConfig(QStringLiteral("trashrc"), KConfig::SimpleConfig); + return trashConfig.group("Status").readEntry("Empty", true); +} + +void FolderModel::undoTextChanged(const QString &text) +{ + if (QAction *action = m_actionCollection.action(QStringLiteral("undo"))) { + action->setText(text); + } +} + +void FolderModel::createFolder() +{ + m_newMenu->setPopupFiles(QList() << m_dirModel->dirLister()->url()); + m_newMenu->createDirectory(); +} + +void FolderModel::setAsWallpaper() +{ + if (!m_selectionModel) + return; + + QUrl url = selectedUrls().first(); + + if (!url.isLocalFile()) + return; + + QDBusInterface iface("org.cutefish.Settings", "/Theme", + "org.cutefish.Theme", + QDBusConnection::sessionBus(), nullptr); + if (iface.isValid()) + iface.call("setWallpaper", url.toLocalFile()); +} + +void FolderModel::openSettings(const QString &itemName) +{ + QProcess process; + process.startDetached("cutefish-settings", QStringList() << "-m" << itemName); +} + +bool FolderModel::desktopView() const +{ + return m_isDesktopView; +} + +void FolderModel::setDesktopView(bool value) +{ + if (m_isDesktopView != value) { + m_isDesktopView = value; + emit desktopViewChanged(); + } +} diff --git a/src/lib/foldermodel.h b/src/lib/foldermodel.h new file mode 100644 index 0000000..266c443 --- /dev/null +++ b/src/lib/foldermodel.h @@ -0,0 +1,372 @@ +/*************************************************************************** + * Copyright (C) 2008 Fredrik Höglund * + * Copyright (C) 2011 Marco Martin * + * Copyright (C) 2014 by Eike Hein * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +#ifndef FOLDERMODEL_H +#define FOLDERMODEL_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +class QDrag; +class QItemSelectionModel; +class QQuickItem; + +class KFileCopyToMenu; +class KActionCollection; +class KDirModel; +class KDirWatch; +class KFileItem; +class KFileItemActions; +class KJob; +class KNewFileMenu; + +namespace KIO +{ +class DropJob; +class StatJob; +} + +class ScreenMapper; + +class DirLister : public KDirLister +{ + Q_OBJECT + +public: + explicit DirLister(QObject *parent = nullptr); + ~DirLister() override; + +Q_SIGNALS: + void error(const QString &string); + +protected: + void handleError(KIO::Job *job) override; +}; + +class FolderModel : public QSortFilterProxyModel, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(QString url READ url WRITE setUrl NOTIFY urlChanged) + Q_PROPERTY(QString iconName READ iconName NOTIFY iconNameChanged) + Q_PROPERTY(QUrl resolvedUrl READ resolvedUrl NOTIFY resolvedUrlChanged) + Q_PROPERTY(Status status READ status NOTIFY statusChanged) + Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged) + Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged) + Q_PROPERTY(bool usedByContainment READ usedByContainment WRITE setUsedByContainment NOTIFY usedByContainmentChanged) + Q_PROPERTY(bool locked READ locked WRITE setLocked NOTIFY lockedChanged) + Q_PROPERTY(int sortMode READ sortMode WRITE setSortMode NOTIFY sortModeChanged) + Q_PROPERTY(bool sortDesc READ sortDesc WRITE setSortDesc NOTIFY sortDescChanged) + Q_PROPERTY(bool sortDirsFirst READ sortDirsFirst WRITE setSortDirsFirst NOTIFY sortDirsFirstChanged) + Q_PROPERTY(bool parseDesktopFiles READ parseDesktopFiles WRITE setParseDesktopFiles NOTIFY parseDesktopFilesChanged) + Q_PROPERTY(QObject *viewAdapter READ viewAdapter WRITE setViewAdapter NOTIFY viewAdapterChanged) + Q_PROPERTY(bool previews READ previews WRITE setPreviews NOTIFY previewsChanged) + Q_PROPERTY(QStringList previewPlugins READ previewPlugins WRITE setPreviewPlugins NOTIFY previewPluginsChanged) + Q_PROPERTY(int filterMode READ filterMode WRITE setFilterMode NOTIFY filterModeChanged) + Q_PROPERTY(QString filterPattern READ filterPattern WRITE setFilterPattern NOTIFY filterPatternChanged) + Q_PROPERTY(QStringList filterMimeTypes READ filterMimeTypes WRITE setFilterMimeTypes NOTIFY filterMimeTypesChanged) + Q_PROPERTY(QObject *newMenu READ newMenu CONSTANT) + Q_PROPERTY(bool desktopView READ desktopView WRITE setDesktopView NOTIFY desktopViewChanged) + +public: + enum DataRole { + BlankRole = Qt::UserRole + 1, + OverlaysRole, + SelectedRole, + IsDirRole, + IsLinkRole, + IsHiddenRole, + UrlRole, + LinkDestinationUrl, + SizeRole, + TypeRole, + FileNameRole, + }; + + enum FilterMode { + NoFilter = 0, + FilterShowMatches, + FilterHideMatches, + }; + + enum Status { + None, + Ready, + Listing, + Canceled, + }; + Q_ENUM(Status) + + explicit FolderModel(QObject *parent = nullptr); + ~FolderModel() override; + + QHash roleNames() const override; + static QHash staticRoleNames(); + + void classBegin() override; + void componentComplete() override; + + QString url() const; + void setUrl(const QString &url); + + QString iconName() const; + + QUrl resolvedUrl() const; + Q_INVOKABLE QUrl resolve(const QString &url); + + Status status() const; + + QString errorString() const; + + bool dragging() const; + + bool usedByContainment() const; + void setUsedByContainment(bool used); + + bool locked() const; + void setLocked(bool locked); + + int sortMode() const; + void setSortMode(int mode); + + bool sortDesc() const; + void setSortDesc(bool desc); + + bool sortDirsFirst() const; + void setSortDirsFirst(bool enable); + + bool parseDesktopFiles() const; + void setParseDesktopFiles(bool enable); + + QObject *viewAdapter() const; + void setViewAdapter(QObject *adapter); + + bool previews() const; + void setPreviews(bool previews); + + QStringList previewPlugins() const; + void setPreviewPlugins(const QStringList &previewPlugins); + + int filterMode() const; + void setFilterMode(int filterMode); + + QString filterPattern() const; + void setFilterPattern(const QString &pattern); + + QStringList filterMimeTypes() const; + void setFilterMimeTypes(const QStringList &mimeList); + + KFileItem rootItem() const; + + Q_INVOKABLE void up(); + Q_INVOKABLE void cd(int row); + + Q_INVOKABLE void run(int row); + Q_INVOKABLE void runSelected(); + + Q_INVOKABLE void rename(int row, const QString &name); + Q_INVOKABLE int fileExtensionBoundary(int row); + + Q_INVOKABLE bool hasSelection() const; + Q_INVOKABLE bool isSelected(int row); + Q_INVOKABLE void setSelected(int row); + Q_INVOKABLE void selectAll(); + Q_INVOKABLE void toggleSelected(int row); + Q_INVOKABLE void setRangeSelected(int anchor, int to); + Q_INVOKABLE void updateSelection(const QVariantList &rows, bool toggle); + Q_INVOKABLE void clearSelection(); + Q_INVOKABLE void pinSelection(); + Q_INVOKABLE void unpinSelection(); + + Q_INVOKABLE void addItemDragImage(int row, int x, int y, int width, int height, const QVariant &image); + Q_INVOKABLE void clearDragImages(); + Q_INVOKABLE void setDragHotSpotScrollOffset(int x, int y); // FIXME TODO: Propify. + Q_INVOKABLE QPoint dragCursorOffset(int row); + Q_INVOKABLE void dragSelected(int x, int y); + Q_INVOKABLE void drop(QQuickItem *target, QObject *dropEvent, int row, bool showMenuManually = false); + Q_INVOKABLE void dropCwd(QObject *dropEvent); + + Q_INVOKABLE bool isBlank(int row) const; + + Q_INVOKABLE QAction *action(const QString &name) const; + QObject *newMenu() const; + Q_INVOKABLE void updateActions(); + Q_INVOKABLE void openContextMenu(QQuickItem *visualParent = nullptr, Qt::KeyboardModifiers modifiers = Qt::NoModifier); + + Q_INVOKABLE void linkHere(const QUrl &sourceUrl); + + Q_INVOKABLE void openPropertiesDialog(); + + Q_INVOKABLE QString desktopPath() const; + Q_INVOKABLE QString homePath() const; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int indexForUrl(const QUrl &url) const; + KFileItem itemForIndex(const QModelIndex &index) const; + bool isDir(const QModelIndex &index, const KDirModel *dirModel) const; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + Qt::DropActions supportedDragActions() const override; + Qt::DropActions supportedDropActions() const override; + + Q_INVOKABLE void paste(); + Q_INVOKABLE void copy(); + Q_INVOKABLE void cut(); + Q_INVOKABLE void deleteSelected(); + Q_INVOKABLE void openSelected(); + Q_INVOKABLE void undo(); + Q_INVOKABLE void refresh(); + Q_INVOKABLE void createFolder(); + Q_INVOKABLE void setAsWallpaper(); + Q_INVOKABLE void openSettings(const QString &itemName); + + bool desktopView() const; + void setDesktopView(bool value); + + void setScreen(int screen); + + bool eventFilter(QObject *watched, QEvent *event) override; + +Q_SIGNALS: + void urlChanged() const; + void listingCompleted() const; + void listingCanceled() const; + void iconNameChanged() const; + void resolvedUrlChanged() const; + void statusChanged() const; + void errorStringChanged() const; + void draggingChanged() const; + void usedByContainmentChanged() const; + void lockedChanged() const; + void sortModeChanged() const; + void sortDescChanged() const; + void sortDirsFirstChanged() const; + void parseDesktopFilesChanged() const; + void viewAdapterChanged(); + void previewsChanged() const; + void previewPluginsChanged() const; + void filterModeChanged() const; + void filterPatternChanged() const; + void filterMimeTypesChanged() const; + void screenChanged() const; + void requestRename() const; + void move(int x, int y, QList urls); + void popupMenuAboutToShow(KIO::DropJob *dropJob, QMimeData *mimeData, int x, int y); + + void desktopViewChanged(); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + bool matchMimeType(const KFileItem &item) const; + bool matchPattern(const KFileItem &item) const; + +private Q_SLOTS: + void dragSelectedInternal(int x, int y); + void dirListFailed(const QString &error); + void statResult(KJob *job); + void evictFromIsDirCache(const KFileItemList &items); + void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + void pasteTo(); + void moveSelectedToTrash(); + void emptyTrashBin(); + void restoreSelectedFromTrash(); + void undoTextChanged(const QString &text); + void invalidateIfComplete(); + void invalidateFilterIfComplete(); + void newFileMenuItemCreated(const QUrl &url); + +private: + struct DragImage { + int row; + QRect rect; + QPoint cursorOffset; + QImage image; + bool blank; + }; + + void createActions(); + void addDragImage(QDrag *drag, int x, int y); + void setStatus(Status status); + static bool isTrashEmpty(); + QList selectedUrls() const; + KDirModel *m_dirModel; + KDirWatch *m_dirWatch; + QString m_url; + mutable QHash m_isDirCache; + mutable QHash m_isDirJobs; + QItemSelectionModel *m_selectionModel; + QItemSelection m_pinnedSelection; + QModelIndexList m_dragIndexes; + QHash m_dragImages; + QPoint m_dragHotSpotScrollOffset; + bool m_dragInProgress; + bool m_urlChangedWhileDragging; + // target filename to target position of a drop event, note that this deliberately + // is not using the URL to easily support desktop:/ URL schemes + QHash m_dropTargetPositions; + QTimer *m_dropTargetPositionsCleanup; + QPointer m_previewGenerator; + QPointer m_viewAdapter; + KActionCollection m_actionCollection; + KNewFileMenu *m_newMenu; + KFileItemActions *m_fileItemActions; + KFileCopyToMenu *m_copyToMenu; + Status m_status = Status::None; + QString m_errorString; + bool m_usedByContainment; + bool m_locked; + int m_sortMode; // FIXME TODO: Enumify. + bool m_sortDesc; + bool m_sortDirsFirst; + bool m_parseDesktopFiles; + bool m_previews; + // An empty previewPlugin list means use default. + // We don't want to leak that fact to the QML side, however, so the property stays empty + // and internally we operate on effectivePreviewPlugins instead. + QStringList m_previewPlugins; + QStringList m_effectivePreviewPlugins; + FilterMode m_filterMode; + QString m_filterPattern; + bool m_filterPatternMatchAll; + QSet m_mimeSet; + QList m_regExps; + int m_screen = -1; + bool m_screenUsed; + bool m_complete; + QPoint m_menuPosition; + + bool m_isDesktopView; +}; + +#endif diff --git a/src/lib/itemviewadapter.cpp b/src/lib/itemviewadapter.cpp new file mode 100644 index 0000000..9493250 --- /dev/null +++ b/src/lib/itemviewadapter.cpp @@ -0,0 +1,126 @@ +/* + * Copyright © 2008 Fredrik Höglund + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "itemviewadapter.h" + +#include +#include +#include + +ItemViewAdapter::ItemViewAdapter(QObject *parent) + : KAbstractViewAdapter(parent) + , m_adapterView(nullptr) + , m_adapterModel(nullptr) + , m_adapterIconSize(-1) +{ +} + +QAbstractItemModel *ItemViewAdapter::model() const +{ + return m_adapterModel; +} + +QSize ItemViewAdapter::iconSize() const +{ + return QSize(m_adapterIconSize, m_adapterIconSize); +} + +QPalette ItemViewAdapter::palette() const +{ + return QPalette(); +} + +QRect ItemViewAdapter::visibleArea() const +{ + return m_adapterVisibleArea; +} + +QRect ItemViewAdapter::visualRect(const QModelIndex &index) const +{ + // FIXME TODO: Implemented on DND branch. + + Q_UNUSED(index) + + return QRect(); +} + +void ItemViewAdapter::connect(Signal signal, QObject *receiver, const char *slot) +{ + if (signal == ScrollBarValueChanged) { + QObject::connect(this, SIGNAL(viewScrolled()), receiver, slot); + } else if (signal == IconSizeChanged) { + QObject::connect(this, SIGNAL(adapterIconSizeChanged()), receiver, slot); + } +} + +QAbstractItemModel *ItemViewAdapter::adapterModel() const +{ + return m_adapterModel; +} + +QObject *ItemViewAdapter::adapterView() const +{ + return m_adapterView; +} + +void ItemViewAdapter::setAdapterView(QObject *view) +{ + if (m_adapterView != view) { + m_adapterView = view; + + emit adapterViewChanged(); + } +} + +void ItemViewAdapter::setAdapterModel(QAbstractItemModel *model) +{ + if (m_adapterModel != model) { + m_adapterModel = model; + + emit adapterModelChanged(); + } +} + +int ItemViewAdapter::adapterIconSize() const +{ + return m_adapterIconSize; +} + +void ItemViewAdapter::setAdapterIconSize(int size) +{ + if (m_adapterIconSize != size) { + m_adapterIconSize = size; + + emit adapterIconSizeChanged(); + } +} + +QRect ItemViewAdapter::adapterVisibleArea() const +{ + return m_adapterVisibleArea; +} + +void ItemViewAdapter::setAdapterVisibleArea(QRect rect) +{ + if (m_adapterVisibleArea != rect) { + m_adapterVisibleArea = rect; + + emit adapterVisibleAreaChanged(); + } +} diff --git a/src/lib/itemviewadapter.h b/src/lib/itemviewadapter.h new file mode 100644 index 0000000..5ec1d97 --- /dev/null +++ b/src/lib/itemviewadapter.h @@ -0,0 +1,72 @@ +/*************************************************************************** + * Copyright (C) 2014 by Eike Hein * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +#ifndef ITEMVIEWADAPTER_H +#define ITEMVIEWADAPTER_H + +#include + +#include + +class ItemViewAdapter : public KAbstractViewAdapter +{ + Q_OBJECT + + Q_PROPERTY(QObject *adapterView READ adapterView WRITE setAdapterView NOTIFY adapterViewChanged) + Q_PROPERTY(QAbstractItemModel *adapterModel READ adapterModel WRITE setAdapterModel NOTIFY adapterModelChanged) + Q_PROPERTY(int adapterIconSize READ adapterIconSize WRITE setAdapterIconSize NOTIFY adapterIconSizeChanged) + Q_PROPERTY(QRect adapterVisibleArea READ adapterVisibleArea WRITE setAdapterVisibleArea NOTIFY adapterVisibleAreaChanged) + +public: + explicit ItemViewAdapter(QObject *parent = nullptr); + + QAbstractItemModel *model() const override; + QSize iconSize() const override; + QPalette palette() const override; + QRect visibleArea() const override; + QRect visualRect(const QModelIndex &index) const override; + void connect(Signal signal, QObject *receiver, const char *slot) override; + + QObject *adapterView() const; + void setAdapterView(QObject *view); + + QAbstractItemModel *adapterModel() const; + void setAdapterModel(QAbstractItemModel *model); + + int adapterIconSize() const; + void setAdapterIconSize(int size); + + QRect adapterVisibleArea() const; + void setAdapterVisibleArea(QRect rect); + +Q_SIGNALS: + void viewScrolled() const; + void adapterViewChanged() const; + void adapterModelChanged() const; + void adapterIconSizeChanged() const; + void adapterVisibleAreaChanged() const; + +private: + QObject *m_adapterView; + QAbstractItemModel *m_adapterModel; + int m_adapterIconSize; + QRect m_adapterVisibleArea; +}; + +#endif diff --git a/src/lib/placesitem.cpp b/src/lib/placesitem.cpp new file mode 100644 index 0000000..71549b0 --- /dev/null +++ b/src/lib/placesitem.cpp @@ -0,0 +1,58 @@ +#include "placesitem.h" +#include + +PlacesItem::PlacesItem(const QString &displayName, + const QString &iconName, + QUrl url, + QObject *parent) + : QObject(parent) + , m_displayName(displayName) + , m_iconName(iconName) + , m_url(url) +{ +} + +QString PlacesItem::displayName() const +{ + return m_displayName; +} + +void PlacesItem::setDisplayName(const QString &name) +{ + m_displayName = name; +} + +QString PlacesItem::iconName() const +{ + return m_iconName; +} + +void PlacesItem::setIconName(const QString &name) +{ + m_iconName = name; +} + +QString PlacesItem::iconPath() const +{ + return m_iconPath; +} + +void PlacesItem::setIconPath(const QString &path) +{ + m_iconPath = path; +} + +QUrl PlacesItem::url() const +{ + return m_url; +} + +void PlacesItem::setUrl(const QUrl &url) +{ + m_url = url; +} + +QString PlacesItem::path() const +{ + return m_url.toString(); +} diff --git a/src/lib/placesitem.h b/src/lib/placesitem.h new file mode 100644 index 0000000..3ea728f --- /dev/null +++ b/src/lib/placesitem.h @@ -0,0 +1,38 @@ +#ifndef PLACESITEM_H +#define PLACESITEM_H + +#include +#include + +class PlacesItem : public QObject +{ + Q_OBJECT + +public: + explicit PlacesItem(const QString &displayName = QString(), + const QString &iconName = QString(), + QUrl url = QUrl(), + QObject *parent = nullptr); + + QString displayName() const; + void setDisplayName(const QString &name); + + QString iconName() const; + void setIconName(const QString &name); + + QString iconPath() const; + void setIconPath(const QString &path); + + QUrl url() const; + void setUrl(const QUrl &url); + + QString path() const; + +private: + QString m_displayName; + QString m_iconName; + QString m_iconPath; + QUrl m_url; +}; + +#endif // PLACESITEM_H diff --git a/src/lib/placesmodel.cpp b/src/lib/placesmodel.cpp new file mode 100644 index 0000000..f893bd7 --- /dev/null +++ b/src/lib/placesmodel.cpp @@ -0,0 +1,156 @@ +#include "placesmodel.h" + +#include +#include + +PlacesModel::PlacesModel(QObject *parent) + : QAbstractItemModel(parent) +{ + const QString homePath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + if (QDir(homePath).exists()) { + PlacesItem *item = new PlacesItem(tr("Home"), "", QUrl::fromLocalFile(homePath)); + item->setIconPath("qrc:/images/folder-home.svg"); + m_items.append(item); + } + + const QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + if (QDir(desktopPath).exists()) { + PlacesItem *item = new PlacesItem(tr("Desktop"), "", QUrl::fromLocalFile(desktopPath)); + item->setIconPath("qrc:/images/folder-desktop.svg"); + m_items.append(item); + } + + const QString documentsPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + if (QDir(documentsPath).exists()) { + PlacesItem *item = new PlacesItem(tr("Documents"), "folder-documents", QUrl::fromLocalFile(documentsPath)); + item->setIconPath("qrc:/images/folder-document.svg"); + m_items.append(item); + } + + const QString downloadPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + if (QDir(downloadPath).exists()) { + PlacesItem *item = new PlacesItem(tr("Downloads"), "folder-downloads", QUrl::fromLocalFile(downloadPath)); + item->setIconPath("qrc:/images/folder-download.svg"); + m_items.append(item); + } + + const QString musicPath = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); + if (QDir(musicPath).exists()) { + PlacesItem *item = new PlacesItem(tr("Music"), "folder-music", QUrl::fromLocalFile(musicPath)); + item->setIconPath("qrc:/images/folder-music.svg"); + m_items.append(item); + } + + const QString picturePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + if (QDir(picturePath).exists()) { + PlacesItem *item = new PlacesItem(tr("Pictures"), "folder-pictures", QUrl::fromLocalFile(picturePath)); + item->setIconPath("qrc:/images/folder-picture.svg"); + m_items.append(item); + } + + const QString videoPath = QStandardPaths::writableLocation(QStandardPaths::MoviesLocation); + if (QDir(videoPath).exists()) { + PlacesItem *item = new PlacesItem(tr("Videos"), "folder-videos", QUrl::fromLocalFile(videoPath)); + item->setIconPath("qrc:/images/folder-video.svg"); + m_items.append(item); + } + + PlacesItem *trashItem = new PlacesItem(tr("Trash"), "", QUrl(QStringLiteral("trash:/"))); + trashItem->setIconPath("qrc:/images/user-trash.svg"); + m_items.append(trashItem); +} + +PlacesModel::~PlacesModel() +{ +} + +QHash PlacesModel::roleNames() const +{ + QHash roleNames; // = QAbstractItemModel::roleNames(); + roleNames[PlacesModel::NameRole] = "name"; + roleNames[PlacesModel::IconNameRole] = "icon"; + roleNames[PlacesModel::IconPathRole] = "iconPath"; + roleNames[PlacesModel::UrlRole] = "url"; + roleNames[PlacesModel::PathRole] = "path"; + return roleNames; +} + +int PlacesModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return m_items.size(); +} + +int PlacesModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 1; +} + +QVariant PlacesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + PlacesItem *item = m_items.at(index.row()); + + switch (role) { + case PlacesModel::NameRole: + return item->displayName(); + break; + case PlacesModel::IconNameRole: + return item->iconName(); + break; + case PlacesModel::IconPathRole: + return item->iconPath(); + break; + case PlacesModel::UrlRole: + return item->url(); + break; + case PlacesModel::PathRole: + return item->path(); + break; + default: + break; + } + + return QVariant(); +} + +QModelIndex PlacesModel::index(int row, int column, const QModelIndex &parent) const +{ + if (row < 0 || column != 0 || row >= m_items.size()) { + return QModelIndex(); + } + + if (parent.isValid()) { + return QModelIndex(); + } + + return createIndex(row, column, m_items.at(row)); +} + +QModelIndex PlacesModel::parent(const QModelIndex &child) const +{ + Q_UNUSED(child); + + return QModelIndex(); +} + +QVariantMap PlacesModel::get(const int &index) const +{ + QVariantMap res; + + if (index >= this->rowCount() || index < 0) + return res; + + const auto roleNames = this->roleNames(); + + for (auto i = roleNames.begin(); i != roleNames.end(); ++i) { + res.insert(i.value(), this->index(index, 0).data(i.key()).toString()); + } + + return res; +} diff --git a/src/lib/placesmodel.h b/src/lib/placesmodel.h new file mode 100644 index 0000000..8361e4a --- /dev/null +++ b/src/lib/placesmodel.h @@ -0,0 +1,39 @@ +#ifndef PLACESMODEL_H +#define PLACESMODEL_H + +#include +#include "placesitem.h" + +class PlacesModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + enum DataRole { + NameRole = Qt::UserRole + 1, + IconNameRole, + IconPathRole, + UrlRole, + PathRole + }; + Q_ENUMS(DataRole); + + explicit PlacesModel(QObject *parent = nullptr); + ~PlacesModel() override; + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + + Q_INVOKABLE QVariantMap get(const int &index) const; + +private: + QList m_items; +}; + +#endif diff --git a/src/lib/positioner.cpp b/src/lib/positioner.cpp new file mode 100644 index 0000000..490369d --- /dev/null +++ b/src/lib/positioner.cpp @@ -0,0 +1,951 @@ +/*************************************************************************** + * Copyright (C) 2014 by Eike Hein * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +#include "positioner.h" +#include "foldermodel.h" + +#include +#include + +#include + +Positioner::Positioner(QObject *parent) + : QAbstractItemModel(parent) + , m_enabled(false) + , m_folderModel(nullptr) + , m_perStripe(0) + , m_ignoreNextTransaction(false) + , m_deferApplyPositions(false) + , m_updatePositionsTimer(new QTimer(this)) +{ + m_updatePositionsTimer->setSingleShot(true); + m_updatePositionsTimer->setInterval(0); + connect(m_updatePositionsTimer, &QTimer::timeout, this, &Positioner::updatePositions); +} + +Positioner::~Positioner() +{ +} + +bool Positioner::enabled() const +{ + return m_enabled; +} + +void Positioner::setEnabled(bool enabled) +{ + if (m_enabled != enabled) { + m_enabled = enabled; + + beginResetModel(); + + if (enabled && m_folderModel) { + initMaps(); + } + + endResetModel(); + + emit enabledChanged(); + + if (!enabled) { + m_updatePositionsTimer->start(); + } + } +} + +FolderModel *Positioner::folderModel() const +{ + return m_folderModel; +} + +void Positioner::setFolderModel(QObject *folderModel) +{ + if (m_folderModel != folderModel) { + beginResetModel(); + + if (m_folderModel) { + disconnectSignals(m_folderModel); + } + + m_folderModel = qobject_cast(folderModel); + + if (m_folderModel) { + connectSignals(m_folderModel); + + if (m_enabled) { + initMaps(); + } + } + + endResetModel(); + + emit folderModelChanged(); + } +} + +int Positioner::perStripe() const +{ + return m_perStripe; +} + +void Positioner::setPerStripe(int perStripe) +{ + if (m_perStripe != perStripe) { + m_perStripe = perStripe; + + emit perStripeChanged(); + + if (m_enabled && perStripe > 0 && !m_proxyToSource.isEmpty()) { + applyPositions(); + } + } +} + +QStringList Positioner::positions() const +{ + return m_positions; +} + +void Positioner::setPositions(const QStringList &positions) +{ + if (m_positions != positions) { + m_positions = positions; + + emit positionsChanged(); + + // Defer applying positions until listing completes. + if (m_folderModel->status() == FolderModel::Listing) { + m_deferApplyPositions = true; + } else { + applyPositions(); + } + } +} + +int Positioner::map(int row) const +{ + if (m_enabled && m_folderModel) { + return m_proxyToSource.value(row, -1); + } + + return row; +} + +int Positioner::nearestItem(int currentIndex, Qt::ArrowType direction) +{ + if (!m_enabled || currentIndex >= rowCount()) { + return -1; + } + + if (currentIndex < 0) { + return firstRow(); + } + + int hDirection = 0; + int vDirection = 0; + + switch (direction) { + case Qt::LeftArrow: + hDirection = -1; + break; + case Qt::RightArrow: + hDirection = 1; + break; + case Qt::UpArrow: + vDirection = -1; + break; + case Qt::DownArrow: + vDirection = 1; + break; + default: + return -1; + } + + QList rows(m_proxyToSource.keys()); + std::sort(rows.begin(), rows.end()); + + int nearestItem = -1; + const QPoint currentPos(currentIndex % m_perStripe, currentIndex / m_perStripe); + int lastDistance = -1; + int distance = 0; + + foreach (int row, rows) { + const QPoint pos(row % m_perStripe, row / m_perStripe); + + if (row == currentIndex) { + continue; + } + + if (hDirection == 0) { + if (vDirection * pos.y() > vDirection * currentPos.y()) { + distance = (pos - currentPos).manhattanLength(); + + if (nearestItem == -1 || distance < lastDistance || (distance == lastDistance && pos.x() == currentPos.x())) { + nearestItem = row; + lastDistance = distance; + } + } + } else if (vDirection == 0) { + if (hDirection * pos.x() > hDirection * currentPos.x()) { + distance = (pos - currentPos).manhattanLength(); + + if (nearestItem == -1 || distance < lastDistance || (distance == lastDistance && pos.y() == currentPos.y())) { + nearestItem = row; + lastDistance = distance; + } + } + } + } + + return nearestItem; +} + +bool Positioner::isBlank(int row) const +{ + if (!m_enabled && m_folderModel) { + return m_folderModel->isBlank(row); + } + + if (m_proxyToSource.contains(row) && m_folderModel && !m_folderModel->isBlank(m_proxyToSource.value(row))) { + return false; + } + + return true; +} + +int Positioner::indexForUrl(const QUrl &url) const +{ + if (!m_folderModel) { + return -1; + } + + const QString &name = url.fileName(); + + int sourceIndex = -1; + + // TODO Optimize. + for (int i = 0; i < m_folderModel->rowCount(); ++i) { + if (m_folderModel->data(m_folderModel->index(i, 0), FolderModel::FileNameRole).toString() == name) { + sourceIndex = i; + + break; + } + } + + return m_sourceToProxy.value(sourceIndex, -1); +} + +void Positioner::setRangeSelected(int anchor, int to) +{ + if (!m_folderModel) { + return; + } + + if (m_enabled) { + QVariantList indices; + + for (int i = qMin(anchor, to); i <= qMax(anchor, to); ++i) { + if (m_proxyToSource.contains(i)) { + indices.append(m_proxyToSource.value(i)); + } + } + + if (!indices.isEmpty()) { + m_folderModel->updateSelection(indices, false); + } + } else { + m_folderModel->setRangeSelected(anchor, to); + } +} + +QHash Positioner::roleNames() const +{ + return FolderModel::staticRoleNames(); +} + +QModelIndex Positioner::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid()) { + return QModelIndex(); + } + + return createIndex(row, column); +} + +QModelIndex Positioner::parent(const QModelIndex &index) const +{ + if (m_folderModel) { + m_folderModel->parent(index); + } + + return QModelIndex(); +} + +QVariant Positioner::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (m_folderModel) { + if (m_enabled) { + if (m_proxyToSource.contains(index.row())) { + return m_folderModel->data(m_folderModel->index(m_proxyToSource.value(index.row()), 0), role); + } else if (role == FolderModel::BlankRole) { + return true; + } + } else { + return m_folderModel->data(m_folderModel->index(index.row(), 0), role); + } + } + + return QVariant(); +} + +int Positioner::rowCount(const QModelIndex &parent) const +{ + if (m_folderModel) { + if (m_enabled) { + if (parent.isValid()) { + return 0; + } else { + return lastRow() + 1; + } + } else { + return m_folderModel->rowCount(parent); + } + } + + return 0; +} + +int Positioner::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + if (m_folderModel) { + return 1; + } + + return 0; +} + +void Positioner::reset() +{ + beginResetModel(); + + initMaps(); + + endResetModel(); + + m_positions = QStringList(); + emit positionsChanged(); +} + +void Positioner::move(const QVariantList &moves) +{ + // Don't allow moves while listing. + if (m_folderModel->status() == FolderModel::Listing) { + m_deferMovePositions = moves; + return; + } + + QVector fromIndices; + QVector toIndices; + QVector sourceRows; + + for (int i = 0; i < moves.count(); ++i) { + const int isFrom = (i % 2 == 0); + const int v = moves[i].toInt(); + + if (isFrom) { + if (m_proxyToSource.contains(v)) { + sourceRows.append(m_proxyToSource.value(v)); + } else { + sourceRows.append(-1); + } + } + + (isFrom ? fromIndices : toIndices).append(v); + } + + const int oldCount = rowCount(); + + for (int i = 0; i < fromIndices.count(); ++i) { + const int from = fromIndices[i]; + int to = toIndices[i]; + const int sourceRow = sourceRows[i]; + + if (sourceRow == -1 || from == to) { + continue; + } + + if (to == -1) { + to = firstFreeRow(); + + if (to == -1) { + to = lastRow() + 1; + } + } + + if (!fromIndices.contains(to) && !isBlank(to)) { + /* find the next blank space + * we won't be happy if we're moving two icons to the same place + */ + while ((!isBlank(to) && from != to) || toIndices.contains(to)) { + to++; + } + } + + toIndices[i] = to; + + if (!toIndices.contains(from)) { + m_proxyToSource.remove(from); + } + + updateMaps(to, sourceRow); + + const QModelIndex &fromIdx = index(from, 0); + emit dataChanged(fromIdx, fromIdx); + + if (to < oldCount) { + const QModelIndex &toIdx = index(to, 0); + emit dataChanged(toIdx, toIdx); + } + } + + const int newCount = rowCount(); + + if (newCount > oldCount) { + if (m_beginInsertRowsCalled) { + endInsertRows(); + m_beginInsertRowsCalled = false; + } + beginInsertRows(QModelIndex(), oldCount, newCount - 1); + endInsertRows(); + } + + if (newCount < oldCount) { + beginRemoveRows(QModelIndex(), newCount, oldCount - 1); + endRemoveRows(); + } + + m_updatePositionsTimer->start(); +} + +void Positioner::updatePositions() +{ + QStringList positions; + + if (m_enabled && !m_proxyToSource.isEmpty() && m_perStripe > 0) { + positions.append(QString::number((1 + ((rowCount() - 1) / m_perStripe)))); + positions.append(QString::number(m_perStripe)); + + QHashIterator it(m_proxyToSource); + + while (it.hasNext()) { + it.next(); + + const QString &name = m_folderModel->data(m_folderModel->index(it.value(), 0), FolderModel::UrlRole).toString(); + + if (name.isEmpty()) { + qDebug() << this << it.value() << "Source model doesn't know this index!"; + + return; + } + + positions.append(name); + positions.append(QString::number(qMax(0, it.key() / m_perStripe))); + positions.append(QString::number(qMax(0, it.key() % m_perStripe))); + } + } + + if (positions != m_positions) { + m_positions = positions; + + emit positionsChanged(); + } +} + +void Positioner::sourceStatusChanged() +{ + if (m_deferApplyPositions && m_folderModel->status() != FolderModel::Listing) { + applyPositions(); + } + + if (m_deferMovePositions.count() && m_folderModel->status() != FolderModel::Listing) { + move(m_deferMovePositions); + m_deferMovePositions.clear(); + } +} + +void Positioner::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) +{ + if (m_enabled) { + int start = topLeft.row(); + int end = bottomRight.row(); + + for (int i = start; i <= end; ++i) { + if (m_sourceToProxy.contains(i)) { + const QModelIndex &idx = index(m_sourceToProxy.value(i), 0); + + emit dataChanged(idx, idx); + } + } + } else { + emit dataChanged(topLeft, bottomRight, roles); + } +} + +void Positioner::sourceModelAboutToBeReset() +{ + emit beginResetModel(); +} + +void Positioner::sourceModelReset() +{ + if (m_enabled) { + initMaps(); + } + + emit endResetModel(); +} + +void Positioner::sourceRowsAboutToBeInserted(const QModelIndex &parent, int start, int end) +{ + if (m_enabled) { + // Don't insert yet if we're waiting for listing to complete to apply + // initial positions; + if (m_deferApplyPositions) { + return; + } else if (m_proxyToSource.isEmpty()) { + beginInsertRows(parent, start, end); + m_beginInsertRowsCalled = true; + + initMaps(end + 1); + + return; + } + + // When new rows are inserted, they might go in the beginning or in the middle. + // In this case we must update first the existing proxy->source and source->proxy + // mapping, otherwise the proxy items will point to the wrong source item. + int count = end - start + 1; + m_sourceToProxy.clear(); + for (auto it = m_proxyToSource.begin(); it != m_proxyToSource.end(); ++it) { + int sourceIdx = *it; + if (sourceIdx >= start) { + *it += count; + } + m_sourceToProxy[*it] = it.key(); + } + + int free = -1; + int rest = -1; + + for (int i = start; i <= end; ++i) { + free = firstFreeRow(); + + if (free != -1) { + updateMaps(free, i); + m_pendingChanges << createIndex(free, 0); + } else { + rest = i; + break; + } + } + + if (rest != -1) { + int firstNew = lastRow() + 1; + int remainder = (end - rest); + + beginInsertRows(parent, firstNew, firstNew + remainder); + m_beginInsertRowsCalled = true; + + for (int i = 0; i <= remainder; ++i) { + updateMaps(firstNew + i, rest + i); + } + } else { + m_ignoreNextTransaction = true; + } + } else { + emit beginInsertRows(parent, start, end); + beginInsertRows(parent, start, end); + m_beginInsertRowsCalled = true; + } +} + +void Positioner::sourceRowsAboutToBeMoved(const QModelIndex &sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex &destinationParent, + int destinationRow) +{ + emit beginMoveRows(sourceParent, sourceStart, sourceEnd, destinationParent, destinationRow); +} + +void Positioner::sourceRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last) +{ + if (m_enabled) { + int oldLast = lastRow(); + + for (int i = first; i <= last; ++i) { + int proxyRow = m_sourceToProxy.take(i); + m_proxyToSource.remove(proxyRow); + m_pendingChanges << createIndex(proxyRow, 0); + } + + QHash newProxyToSource; + QHash newSourceToProxy; + QHashIterator it(m_sourceToProxy); + int delta = std::abs(first - last) + 1; + + while (it.hasNext()) { + it.next(); + + if (it.key() > last) { + newProxyToSource.insert(it.value(), it.key() - delta); + newSourceToProxy.insert(it.key() - delta, it.value()); + } else { + newProxyToSource.insert(it.value(), it.key()); + newSourceToProxy.insert(it.key(), it.value()); + } + } + + m_proxyToSource = newProxyToSource; + m_sourceToProxy = newSourceToProxy; + + int newLast = lastRow(); + + if (oldLast > newLast) { + int diff = oldLast - newLast; + beginRemoveRows(QModelIndex(), ((oldLast - diff) + 1), oldLast); + } else { + m_ignoreNextTransaction = true; + } + } else { + emit beginRemoveRows(parent, first, last); + } +} + +void Positioner::sourceLayoutAboutToBeChanged(const QList &parents, QAbstractItemModel::LayoutChangeHint hint) +{ + Q_UNUSED(parents) + + emit layoutAboutToBeChanged(QList(), hint); +} + +void Positioner::sourceRowsInserted(const QModelIndex &parent, int first, int last) +{ + Q_UNUSED(parent) + Q_UNUSED(first) + Q_UNUSED(last) + + if (!m_ignoreNextTransaction) { + if (m_beginInsertRowsCalled) { + endInsertRows(); + m_beginInsertRowsCalled = false; + } + } else { + m_ignoreNextTransaction = false; + } + + flushPendingChanges(); + + // Don't generate new positions data if we're waiting for listing to + // complete to apply initial positions. + if (!m_deferApplyPositions) { + m_updatePositionsTimer->start(); + } +} + +void Positioner::sourceRowsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow) +{ + Q_UNUSED(sourceParent) + Q_UNUSED(sourceStart) + Q_UNUSED(sourceEnd) + Q_UNUSED(destinationParent) + Q_UNUSED(destinationRow) + + emit endMoveRows(); +} + +void Positioner::sourceRowsRemoved(const QModelIndex &parent, int first, int last) +{ + Q_UNUSED(parent) + Q_UNUSED(first) + Q_UNUSED(last) + + if (!m_ignoreNextTransaction) { + emit endRemoveRows(); + } else { + m_ignoreNextTransaction = false; + } + + flushPendingChanges(); + + m_updatePositionsTimer->start(); +} + +void Positioner::sourceLayoutChanged(const QList &parents, QAbstractItemModel::LayoutChangeHint hint) +{ + Q_UNUSED(parents) + + if (m_enabled) { + initMaps(); + } + + emit layoutChanged(QList(), hint); +} + +void Positioner::initMaps(int size) +{ + m_proxyToSource.clear(); + m_sourceToProxy.clear(); + + if (size == -1) { + size = m_folderModel->rowCount(); + } + + if (!size) { + return; + } + + for (int i = 0; i < size; ++i) { + updateMaps(i, i); + } +} + +void Positioner::updateMaps(int proxyIndex, int sourceIndex) +{ + m_proxyToSource.insert(proxyIndex, sourceIndex); + m_sourceToProxy.insert(sourceIndex, proxyIndex); +} + +int Positioner::firstRow() const +{ + if (!m_proxyToSource.isEmpty()) { + QList keys(m_proxyToSource.keys()); + std::sort(keys.begin(), keys.end()); + + return keys.first(); + } + + return -1; +} + +int Positioner::lastRow() const +{ + if (!m_proxyToSource.isEmpty()) { + QList keys(m_proxyToSource.keys()); + std::sort(keys.begin(), keys.end()); + return keys.last(); + } + + return 0; +} + +int Positioner::firstFreeRow() const +{ + if (!m_proxyToSource.isEmpty()) { + int last = lastRow(); + + for (int i = 0; i <= last; ++i) { + if (!m_proxyToSource.contains(i)) { + return i; + } + } + } + + return -1; +} + +void Positioner::applyPositions() +{ + // We were called while the source model is listing. Defer applying positions + // until listing completes. + if (m_folderModel->status() == FolderModel::Listing) { + m_deferApplyPositions = true; + + return; + } + + if (m_positions.size() < 5) { + // We were waiting for listing to complete before proxying source rows, + // but we don't have positions to apply. Reset to populate. + if (m_deferApplyPositions) { + m_deferApplyPositions = false; + reset(); + } + + return; + } + + beginResetModel(); + + m_proxyToSource.clear(); + m_sourceToProxy.clear(); + + const QStringList &positions = m_positions.mid(2); + + if (positions.count() % 3 != 0) { + return; + } + + QHash sourceIndices; + + for (int i = 0; i < m_folderModel->rowCount(); ++i) { + sourceIndices.insert(m_folderModel->data(m_folderModel->index(i, 0), FolderModel::UrlRole).toString(), i); + } + + QString name; + int stripe = -1; + int pos = -1; + int sourceIndex = -1; + int index = -1; + bool ok = false; + int offset = 0; + + // Restore positions for items that still fit. + for (int i = 0; i < positions.count() / 3; ++i) { + offset = i * 3; + pos = positions.at(offset + 2).toInt(&ok); + if (!ok) { + return; + } + + if (pos <= m_perStripe) { + name = positions.at(offset); + stripe = positions.at(offset + 1).toInt(&ok); + if (!ok) { + return; + } + + if (!sourceIndices.contains(name)) { + continue; + } else { + sourceIndex = sourceIndices.value(name); + } + + index = (stripe * m_perStripe) + pos; + + if (m_proxyToSource.contains(index)) { + continue; + } + + updateMaps(index, sourceIndex); + sourceIndices.remove(name); + } + } + + // Find new positions for items that didn't fit. + for (int i = 0; i < positions.count() / 3; ++i) { + offset = i * 3; + pos = positions.at(offset + 2).toInt(&ok); + if (!ok) { + return; + } + + if (pos > m_perStripe) { + name = positions.at(offset); + + if (!sourceIndices.contains(name)) { + continue; + } else { + sourceIndex = sourceIndices.take(name); + } + + index = firstFreeRow(); + + if (index == -1) { + index = lastRow() + 1; + } + + updateMaps(index, sourceIndex); + } + } + + QHashIterator it(sourceIndices); + + // Find positions for new source items we don't have records for. + while (it.hasNext()) { + it.next(); + + index = firstFreeRow(); + + if (index == -1) { + index = lastRow() + 1; + } + + updateMaps(index, it.value()); + } + + endResetModel(); + + m_deferApplyPositions = false; + + m_updatePositionsTimer->start(); +} + +void Positioner::flushPendingChanges() +{ + if (m_pendingChanges.isEmpty()) { + return; + } + + int last = lastRow(); + + foreach (const QModelIndex &idx, m_pendingChanges) { + if (idx.row() <= last) { + emit dataChanged(idx, idx); + } + } + + m_pendingChanges.clear(); +} + +void Positioner::connectSignals(FolderModel *model) +{ + connect(model, &QAbstractItemModel::dataChanged, this, &Positioner::sourceDataChanged, Qt::UniqueConnection); + connect(model, &QAbstractItemModel::rowsAboutToBeInserted, this, &Positioner::sourceRowsAboutToBeInserted, Qt::UniqueConnection); + connect(model, &QAbstractItemModel::rowsAboutToBeMoved, this, &Positioner::sourceRowsAboutToBeMoved, Qt::UniqueConnection); + connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &Positioner::sourceRowsAboutToBeRemoved, Qt::UniqueConnection); + connect(model, &QAbstractItemModel::layoutAboutToBeChanged, this, &Positioner::sourceLayoutAboutToBeChanged, Qt::UniqueConnection); + connect(model, &QAbstractItemModel::rowsInserted, this, &Positioner::sourceRowsInserted, Qt::UniqueConnection); + connect(model, &QAbstractItemModel::rowsMoved, this, &Positioner::sourceRowsMoved, Qt::UniqueConnection); + connect(model, &QAbstractItemModel::rowsRemoved, this, &Positioner::sourceRowsRemoved, Qt::UniqueConnection); + connect(model, &QAbstractItemModel::layoutChanged, this, &Positioner::sourceLayoutChanged, Qt::UniqueConnection); + connect(m_folderModel, &FolderModel::urlChanged, this, &Positioner::reset, Qt::UniqueConnection); + connect(m_folderModel, &FolderModel::statusChanged, this, &Positioner::sourceStatusChanged, Qt::UniqueConnection); +} + +void Positioner::disconnectSignals(FolderModel *model) +{ + disconnect(model, &QAbstractItemModel::dataChanged, this, &Positioner::sourceDataChanged); + disconnect(model, &QAbstractItemModel::rowsAboutToBeInserted, this, &Positioner::sourceRowsAboutToBeInserted); + disconnect(model, &QAbstractItemModel::rowsAboutToBeMoved, this, &Positioner::sourceRowsAboutToBeMoved); + disconnect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &Positioner::sourceRowsAboutToBeRemoved); + disconnect(model, &QAbstractItemModel::layoutAboutToBeChanged, this, &Positioner::sourceLayoutAboutToBeChanged); + disconnect(model, &QAbstractItemModel::rowsInserted, this, &Positioner::sourceRowsInserted); + disconnect(model, &QAbstractItemModel::rowsMoved, this, &Positioner::sourceRowsMoved); + disconnect(model, &QAbstractItemModel::rowsRemoved, this, &Positioner::sourceRowsRemoved); + disconnect(model, &QAbstractItemModel::layoutChanged, this, &Positioner::sourceLayoutChanged); + disconnect(m_folderModel, &FolderModel::urlChanged, this, &Positioner::reset); + disconnect(m_folderModel, &FolderModel::statusChanged, this, &Positioner::sourceStatusChanged); +} diff --git a/src/lib/positioner.h b/src/lib/positioner.h new file mode 100644 index 0000000..6f3e73a --- /dev/null +++ b/src/lib/positioner.h @@ -0,0 +1,140 @@ +/*************************************************************************** + * Copyright (C) 2014 by Eike Hein * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +#ifndef POSITIONER_H +#define POSITIONER_H + +#include + +class FolderModel; + +class QTimer; + +class Positioner : public QAbstractItemModel +{ + Q_OBJECT + + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(FolderModel *folderModel READ folderModel WRITE setFolderModel NOTIFY folderModelChanged) + Q_PROPERTY(int perStripe READ perStripe WRITE setPerStripe NOTIFY perStripeChanged) + Q_PROPERTY(QStringList positions READ positions WRITE setPositions NOTIFY positionsChanged) + +public: + explicit Positioner(QObject *parent = nullptr); + ~Positioner() override; + + bool enabled() const; + void setEnabled(bool enabled); + + FolderModel *folderModel() const; + void setFolderModel(QObject *folderModel); + + int perStripe() const; + void setPerStripe(int perStripe); + + QStringList positions() const; + void setPositions(const QStringList &positions); + + Q_INVOKABLE int map(int row) const; + + Q_INVOKABLE int nearestItem(int currentIndex, Qt::ArrowType direction); + + Q_INVOKABLE bool isBlank(int row) const; + Q_INVOKABLE int indexForUrl(const QUrl &url) const; + + Q_INVOKABLE void setRangeSelected(int anchor, int to); + + Q_INVOKABLE void reset(); + + Q_INVOKABLE void move(const QVariantList &moves); + + QHash roleNames() const override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + +#ifdef BUILD_TESTING + QHash proxyToSourceMapping() const + { + return m_proxyToSource; + } + QHash sourceToProxyMapping() const + { + return m_sourceToProxy; + } +#endif + +Q_SIGNALS: + void enabledChanged() const; + void folderModelChanged() const; + void perStripeChanged() const; + void positionsChanged() const; + +private Q_SLOTS: + void updatePositions(); + void sourceStatusChanged(); + void sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); + void sourceModelAboutToBeReset(); + void sourceModelReset(); + void sourceRowsAboutToBeInserted(const QModelIndex &parent, int start, int end); + void sourceRowsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow); + void sourceRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last); + void sourceLayoutAboutToBeChanged(const QList &parents, QAbstractItemModel::LayoutChangeHint hint); + void sourceRowsInserted(const QModelIndex &parent, int first, int last); + void sourceRowsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow); + void sourceRowsRemoved(const QModelIndex &parent, int first, int last); + void sourceLayoutChanged(const QList &parents, QAbstractItemModel::LayoutChangeHint hint); + +private: + void initMaps(int size = -1); + void updateMaps(int proxyIndex, int sourceIndex); + int firstRow() const; + int lastRow() const; + int firstFreeRow() const; + void applyPositions(); + void flushPendingChanges(); + void connectSignals(FolderModel *model); + void disconnectSignals(FolderModel *model); + + bool m_enabled; + FolderModel *m_folderModel; + + int m_perStripe; + + int m_lastRow; + + QModelIndexList m_pendingChanges; + bool m_ignoreNextTransaction; + + QStringList m_positions; + bool m_deferApplyPositions; + QVariantList m_deferMovePositions; + QTimer *m_updatePositionsTimer; + + QHash m_proxyToSource; + QHash m_sourceToProxy; + bool m_beginInsertRowsCalled = false; // used to sync the amount of begin/endInsertRows calls +}; + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..6f2a671 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2021 CutefishOS 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 + +#include "fmlist.h" +#include "fm.h" +#include "basemodel.h" +#include "baselist.h" +#include "handy.h" +#include "placeslist.h" +#include "pathlist.h" + +#include "desktop/desktopsettings.h" +#include "desktop/desktopview.h" +#include "rubberband.h" + +#include "lib/foldermodel.h" +#include "lib/placesmodel.h" +#include "lib/itemviewadapter.h" +#include "lib/positioner.h" + +int main(int argc, char *argv[]) +{ + const char *uri = "Cutefish.FileManager"; + + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + + QApplication app(argc, argv); + app.setOrganizationName("cutefishos"); + + // Translations + QLocale locale; + QString qmFilePath = QString("%1/%2.qm").arg("/usr/share/cutefish-filemanager/translations/").arg(locale.name()); + if (QFile::exists(qmFilePath)) { + QTranslator *translator = new QTranslator(app.instance()); + if (translator->load(qmFilePath)) { + app.installTranslator(translator); + } else { + translator->deleteLater(); + } + } + + QCommandLineParser parser; + parser.setApplicationDescription(QStringLiteral("File Manager")); + parser.addHelpOption(); + + QCommandLineOption desktopOption(QStringList() << "d" << "desktop" << "Desktop Mode"); + parser.addOption(desktopOption); + parser.process(app); + + qmlRegisterAnonymousType(uri, 1); + qmlRegisterType(uri, 1, 0, "DesktopSettings"); + qmlRegisterType(uri, 1, 0, "RubberBand"); + + qmlRegisterType(uri, 1, 0, "FolderModel"); + qmlRegisterType(uri, 1, 0, "ItemViewAdapter"); + qmlRegisterType(uri, 1, 0, "Positioner"); + + qmlRegisterType(uri, 1, 0, "PlacesModel"); + + if (parser.isSet(desktopOption)) { + DesktopView view; + view.show(); + + return app.exec(); + } + + qmlRegisterAnonymousType(uri, 1); // ABSTRACT BASE LIST + qmlRegisterType(uri, 1, 0, "BaseModel"); // BASE MODEL + qmlRegisterType(uri, 1, 0, "PlacesList"); + qmlRegisterType(uri, 1, 0, "PathList"); + + qmlRegisterType(uri, 1, 0, "FMList"); + qmlRegisterSingletonType(uri, 1, 0, "FM", [](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject * { + Q_UNUSED(engine) + Q_UNUSED(scriptEngine) + return new FMStatic; + }); + + qmlRegisterSingletonType(uri, 1, 0, "Handy", [](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject * { + Q_UNUSED(engine) + Q_UNUSED(scriptEngine) + return new Handy; + }); + + QQmlApplicationEngine engine; + const QUrl url(QStringLiteral("qrc:/qml/main.qml")); + QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, + &app, [url](QObject *obj, const QUrl &objUrl) { + if (!obj && url == objUrl) + QCoreApplication::exit(-1); + }, Qt::QueuedConnection); + engine.load(url); + + return app.exec(); +} diff --git a/src/pathlist.cpp b/src/pathlist.cpp new file mode 100644 index 0000000..47bbaca --- /dev/null +++ b/src/pathlist.cpp @@ -0,0 +1,126 @@ +/* + * + * Copyright (C) 2019 camilo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "pathlist.h" + +PathList::PathList(QObject *parent) + : BaseList(parent) +{ +} + +QVariantMap PathList::get(const int &index) const +{ + if (this->list.isEmpty() || index >= this->list.size() || index < 0) { + return QVariantMap(); + } + + const auto model = this->list.at(index); + return FMH::toMap(model); +} + +QString PathList::getPath() const +{ + return this->m_path; +} + +const FMH::MODEL_LIST &PathList::items() const +{ + return this->list; +} + +void PathList::setList() +{ + const auto paths = PathList::splitPath(m_path); + + if (this->list.isEmpty()) { + emit this->preListChanged(); + this->list << paths; + emit this->postListChanged(); + } else { + const int index = [&]() -> int { + int i = 0; + for (const auto &item : qAsConst(list)) { + if (i < paths.size()) { + if (item[FMH::MODEL_KEY::PATH] != paths[i][FMH::MODEL_KEY::PATH]) { + break; + } else + i++; + } else + break; + } + return i; + }(); + + for (auto i = this->list.size() - 1; i >= index; i--) { + emit preItemRemoved(i); + this->list.removeAt(i); + emit postItemRemoved(); + } + + for (auto i = index; i < paths.size(); i++) { + emit preItemAppended(); + this->list << paths[i]; + emit postItemAppended(); + } + } +} + +void PathList::setPath(const QString &path) +{ + if (path == this->m_path) + return; + + this->m_path = path; + this->setList(); + + emit this->pathChanged(); + + qDebug() << this->list; +} + +FMH::MODEL_LIST PathList::splitPath(const QString &path) +{ + FMH::MODEL_LIST res; + + QString _url = path; + + while (_url.endsWith("/")) + _url.chop(1); + + _url += "/"; + + const auto count = _url.count("/"); + + for (auto i = 0; i < count; i++) { + _url = QString(_url).left(_url.lastIndexOf("/")); + auto label = QString(_url).right(_url.length() - _url.lastIndexOf("/") - 1); + + if (label.isEmpty()) + continue; + + if (label.contains(":") && i == count - 1) // handle the protocol + { + res << FMH::MODEL {{FMH::MODEL_KEY::LABEL, "/"}, {FMH::MODEL_KEY::PATH, _url + "///"}}; + break; + } + + res << FMH::MODEL {{FMH::MODEL_KEY::LABEL, label}, {FMH::MODEL_KEY::PATH, _url}}; + } + std::reverse(res.begin(), res.end()); + return res; +} diff --git a/src/pathlist.h b/src/pathlist.h new file mode 100644 index 0000000..afc7d37 --- /dev/null +++ b/src/pathlist.h @@ -0,0 +1,71 @@ +/* + * + * Copyright (C) 2019 camilo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef PATHLIST_H +#define PATHLIST_H + +#include "baselist.h" + +/** + * @brief The PathList class + */ +class PathList : public BaseList +{ + Q_OBJECT + + Q_PROPERTY(QString path READ getPath WRITE setPath NOTIFY pathChanged) + +public: + PathList(QObject *parent = nullptr); + + const FMH::MODEL_LIST &items() const override; + + /** + * @brief setPath + * @param path + */ + void setPath(const QString &path); + + /** + * @brief getPath + * @return + */ + QString getPath() const; + + /** + * @brief get + * @param index + * @return + */ + QVariantMap get(const int &index) const; + +private: + FMH::MODEL_LIST list; + QString m_path; + + static FMH::MODEL_LIST splitPath(const QString &path); + void setList(); + +signals: + /** + * @brief pathChanged + */ + void pathChanged(); +}; + +#endif // PATHLIST_H diff --git a/src/placeslist.cpp b/src/placeslist.cpp new file mode 100644 index 0000000..ab402ab --- /dev/null +++ b/src/placeslist.cpp @@ -0,0 +1,233 @@ +/* + * + * Copyright (C) 2018 camilo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "placeslist.h" +#include "fm.h" + +#include +#include +#include +#include + +#include + +PlacesList::PlacesList(QObject *parent) + : BaseList(parent) + , fm(new FM(this)) + , model(new KFilePlacesModel(this)) + , watcher(new QFileSystemWatcher(this)) +{ + /* + * The watcher signal returns a local file URL withouth a scheme, and the model is using a local file URL with file:// scheme. + * So those need to be correctly mapped + * */ + connect(watcher, &QFileSystemWatcher::directoryChanged, [&](const QString &path) { + if (this->count.contains(QUrl::fromLocalFile(path).toString())) { + const auto oldCount = this->count[QUrl::fromLocalFile(path).toString()]; + const auto index = this->indexOf(FMH::MODEL_KEY::PATH, QUrl::fromLocalFile(path).toString()); + const QDir dir(path); + const auto newCount = dir.count(); + int count = newCount - oldCount; + + this->list[index][FMH::MODEL_KEY::COUNT] = QString::number(std::max(0, count)); + emit this->updateModel(index, {FMH::MODEL_KEY::COUNT}); + } + }); + + connect(this->model, &KFilePlacesModel::reloaded, [this]() { + this->setList(); + }); + + connect(this->model, &KFilePlacesModel::rowsInserted, [this](const QModelIndex, int, int) { + this->setList(); + emit this->bookmarksChanged(); + + /*emit this->preListChanged(); + + for (int i = first; i <= last; i++) + { + const QModelIndex index = model->index(i, 0); + + if(this->groups.contains(model->groupType(index))) + { + this->list << getGroup(*this->model, static_cast(model->groupType(index))); + } + } + emit this->postListChanged(); */ + }); // TODO improve the usage of the model +} + +void PlacesList::watchPath(const QString &path) +{ + if (path.isEmpty() || !FMH::fileExists(path) || !QUrl(path).isLocalFile()) + return; + + this->watcher->addPath(QUrl(path).toLocalFile()); +} + +void PlacesList::componentComplete() +{ + connect(this, &PlacesList::groupsChanged, this, &PlacesList::setList); + this->setList(); +} + +const FMH::MODEL_LIST &PlacesList::items() const +{ + return this->list; +} + +FMH::MODEL_LIST PlacesList::getGroup(const KFilePlacesModel &model, const FMH::PATHTYPE_KEY &type) +{ + FMH::MODEL_LIST res; + + if (type == FMH::PATHTYPE_KEY::QUICK_PATH) { + res << FMH::MODEL {{FMH::MODEL_KEY::PATH, FMH::PATHTYPE_URI[FMH::PATHTYPE_KEY::TAGS_PATH] + "fav"}, {FMH::MODEL_KEY::ICON, "love"}, {FMH::MODEL_KEY::LABEL, "Favorite"}, {FMH::MODEL_KEY::TYPE, "Quick"}}; + +#if defined Q_OS_LINUX && !defined Q_OS_ANDROID + res << FMH::MODEL {{FMH::MODEL_KEY::PATH, "recentdocuments:///"}, {FMH::MODEL_KEY::ICON, "view-media-recent"}, {FMH::MODEL_KEY::LABEL, "Recent"}, {FMH::MODEL_KEY::TYPE, "Quick"}}; +#endif + + return res; + } + + if (type == FMH::PATHTYPE_KEY::PLACES_PATH) { + res << FMStatic::getDefaultPaths(); + } + + const auto group = model.groupIndexes(static_cast(type)); + res << std::accumulate(group.constBegin(), group.constEnd(), FMH::MODEL_LIST(), [&model, &type](FMH::MODEL_LIST &list, const QModelIndex &index) -> FMH::MODEL_LIST { + const QUrl url = model.url(index); + if (type == FMH::PATHTYPE_KEY::PLACES_PATH && FMH::defaultPaths.contains(url.toString())) + return list; + + if (type == FMH::PATHTYPE_KEY::PLACES_PATH && url.isLocalFile() && !FMH::fileExists(url)) + return list; + + list << FMH::MODEL {{FMH::MODEL_KEY::PATH, url.toString()}, + {FMH::MODEL_KEY::URL, url.toString()}, + {FMH::MODEL_KEY::ICON, model.icon(index).name()}, + {FMH::MODEL_KEY::LABEL, model.text(index)}, + {FMH::MODEL_KEY::NAME, model.text(index)}, + {FMH::MODEL_KEY::TYPE, type == FMH::PATHTYPE_KEY::PLACES_PATH ? FMH::PATHTYPE_LABEL[FMH::PATHTYPE_KEY::BOOKMARKS_PATH] : FMH::PATHTYPE_LABEL[type]}}; + + return list; + }); + + return res; +} + +void PlacesList::setList() +{ + if (this->groups.isEmpty()) + return; + + emit this->preListChanged(); + + this->list.clear(); + + for (const auto &group : qAsConst(this->groups)) { + switch (group) { + case FMH::PATHTYPE_KEY::PLACES_PATH: + this->list << getGroup(*this->model, FMH::PATHTYPE_KEY::PLACES_PATH); + break; + + case FMH::PATHTYPE_KEY::QUICK_PATH: + this->list << getGroup(*this->model, FMH::PATHTYPE_KEY::QUICK_PATH); + break; + + case FMH::PATHTYPE_KEY::APPS_PATH: + this->list << FM::getAppsPath(); + break; + + case FMH::PATHTYPE_KEY::DRIVES_PATH: + this->list << getGroup(*this->model, FMH::PATHTYPE_KEY::DRIVES_PATH); + break; + + case FMH::PATHTYPE_KEY::REMOTE_PATH: + this->list << getGroup(*this->model, FMH::PATHTYPE_KEY::REMOTE_PATH); + break; + + case FMH::PATHTYPE_KEY::REMOVABLE_PATH: + this->list << getGroup(*this->model, FMH::PATHTYPE_KEY::REMOVABLE_PATH); + break; + } + } + + this->setCount(); + emit this->postListChanged(); +} + +void PlacesList::setCount() +{ + this->watcher->removePaths(this->watcher->directories()); + for (auto &data : this->list) { + const auto path = data[FMH::MODEL_KEY::URL]; + if (FMStatic::isDir(path)) { + data.insert(FMH::MODEL_KEY::COUNT, "0"); + QDir dir(QUrl(path).toLocalFile()); + const auto count = dir.count(); + this->count.insert(path, count); + this->watchPath(path); + } + } +} + +QList PlacesList::getGroups() const +{ + return this->groups; +} + +void PlacesList::setGroups(const QList &value) +{ + if (this->groups == value) + return; + + this->groups = value; + emit this->groupsChanged(); +} + +QVariantMap PlacesList::get(const int &index) const +{ + if (index >= this->list.size() || index < 0) + return QVariantMap(); + + const auto model = this->list.at(index); + return FMH::toMap(model); +} + +void PlacesList::clearBadgeCount(const int &index) +{ + this->list[index][FMH::MODEL_KEY::COUNT] = "0"; + emit this->updateModel(index, {FMH::MODEL_KEY::COUNT}); +} + +void PlacesList::removePlace(const int &index) +{ + if (index >= this->list.size() || index < 0) + return; + + emit this->preItemRemoved(index); + this->model->removePlace(this->model->closestItem(this->list.at(index)[FMH::MODEL_KEY::PATH])); + this->list.removeAt(index); + emit this->postItemRemoved(); +} + +bool PlacesList::contains(const QUrl &path) +{ + return this->exists(FMH::MODEL_KEY::PATH, path.toString()); +} diff --git a/src/placeslist.h b/src/placeslist.h new file mode 100644 index 0000000..e050286 --- /dev/null +++ b/src/placeslist.h @@ -0,0 +1,102 @@ +/* + * + * Copyright (C) 2018 camilo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef PLACESLIST_H +#define PLACESLIST_H + +#include "baselist.h" +#include + +class FM; +class KFilePlacesModel; +class QFileSystemWatcher; +class PlacesList : public BaseList +{ + Q_OBJECT + Q_PROPERTY(QList groups READ getGroups WRITE setGroups NOTIFY groupsChanged) + +public: + PlacesList(QObject *parent = nullptr); + + const FMH::MODEL_LIST &items() const override; + + QList getGroups() const; + void setGroups(const QList &value); + + void componentComplete() override final; + + /** + * @brief get + * Gets a item in the model. + * @param index + * Index of the item in the model. The given index is not mapped to a filtered or sorted model + * @return + * The data of the place + */ + QVariantMap get(const int &index) const; + +protected: + void setList(); + void reset(); + +public slots: + /** + * @brief clearBadgeCount + * Clears the count associated to a place at a given index in the model + * @param index + */ + void clearBadgeCount(const int &index); + + /** + * @brief removePlace + * Removes a place from the model and if the data at the given index is a file URL bookmark then it gets removed from the bookmarks. + * @param index + * Index of the item to be removed in the model + */ + void removePlace(const int &index); + + /** + * @brief contains + * Checks of a file URL exists in the places model + * @param path + * File URL to be checked + * @return + * True if it exists otherwise false + */ + bool contains(const QUrl &path); + +private: + FM *fm; + FMH::MODEL_LIST list; + KFilePlacesModel *model; + QHash count; + + QList groups; + + QFileSystemWatcher *watcher; + void watchPath(const QString &path); + + void setCount(); + + static FMH::MODEL_LIST getGroup(const KFilePlacesModel &model, const FMH::PATHTYPE_KEY &type); + +signals: + void groupsChanged(); + void bookmarksChanged(); +}; +#endif // PLACESLIST_H diff --git a/src/rubberband.cpp b/src/rubberband.cpp new file mode 100644 index 0000000..3bd1048 --- /dev/null +++ b/src/rubberband.cpp @@ -0,0 +1,64 @@ +#include "rubberband.h" + +#include +#include +#include + +RubberBand::RubberBand(QQuickItem *parent) + : QQuickPaintedItem(parent) +{ +} + +RubberBand::~RubberBand() +{ +} + +void RubberBand::paint(QPainter *painter) +{ + if (!qApp) { + return; + } + + QPalette palette; + palette.setColor(QPalette::Highlight, m_color); + + QStyleOptionRubberBand opt; + opt.state = QStyle::State_None; + opt.direction = qApp->layoutDirection(); + opt.styleObject = this; + opt.palette = palette; + opt.shape = QRubberBand::Rectangle; + opt.opaque = false; + opt.rect = contentsBoundingRect().toRect(); + qApp->style()->drawControl(QStyle::CE_RubberBand, &opt, painter); +} + +bool RubberBand::intersects(const QRectF &rect) const +{ + return m_geometry.intersects(rect); +} + +QColor RubberBand::color() const +{ + return m_color; +} + +void RubberBand::setColor(QColor color) +{ + if (m_color != color) { + m_color = color; + update(); + emit colorChanged(); + } +} + +void RubberBand::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + Q_UNUSED(oldGeometry); + + m_geometry = newGeometry; + + update(); + + QQuickItem::geometryChanged(newGeometry, oldGeometry); +} diff --git a/src/rubberband.h b/src/rubberband.h new file mode 100644 index 0000000..24ee2d2 --- /dev/null +++ b/src/rubberband.h @@ -0,0 +1,33 @@ +#ifndef RUBBERBAND_H +#define RUBBERBAND_H + +#include + +class RubberBand : public QQuickPaintedItem +{ + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + +public: + explicit RubberBand(QQuickItem *parent = nullptr); + ~RubberBand() override; + + void paint(QPainter *painter) override; + + Q_INVOKABLE bool intersects(const QRectF &rect) const; + + QColor color() const; + void setColor(QColor color); + +signals: + void colorChanged(); + +protected: + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + +private: + QRectF m_geometry; + QColor m_color; +}; + +#endif diff --git a/translations/en_US.ts b/translations/en_US.ts new file mode 100644 index 0000000..6721291 --- /dev/null +++ b/translations/en_US.ts @@ -0,0 +1,283 @@ + + + + + BrowserMenu + + + New Folder + + + + + Paste + + + + + Open in Terminal + + + + + Select All + + + + + Properties + + + + + Empty Trash + + + + + BrowserView + + + No Files + + + + + DesktopView + + + Desktop + + + + + FolderModel + + + Cut + + + + + Copy + + + + + Undo + + + + + Paste + + + + + New Folder + + + + + New Documents + + + + + Rename + + + + + Move To Trash + + + + + &Empty Trash + + + + + Restore from trash + + + + + Delete + + + + + &Open + + + + + Select All + + + + + Change Wallpaper + + + + + Properties + + + + + Set as Wallpaper + + + + + &Properties + + + + + ItemMenu + + + Open + + + + + Copy + + + + + Cut + + + + + Move to Trash + + + + + Rename + + + + + Open in Terminal + + + + + Set As Wallpaper + + + + + Properties + + + + + PlacesModel + + + Home + + + + + Desktop + + + + + Documents + + + + + Downloads + + + + + Music + + + + + Pictures + + + + + Videos + + + + + Trash + + + + + PropertiesDialog + + + Properties + + + + + Type: + + + + + Location: + + + + + Size: + + + + + Created: + + + + + Modified: + + + + + Accessed: + + + + + Cancel + + + + + OK + + + + + main + + + File Manager + + + + diff --git a/translations/zh_CN.ts b/translations/zh_CN.ts new file mode 100644 index 0000000..a0d81c5 --- /dev/null +++ b/translations/zh_CN.ts @@ -0,0 +1,283 @@ + + + + + BrowserMenu + + + New Folder + 新建文件夹 + + + + Paste + 粘贴 + + + + Open in Terminal + 在终端中打开 + + + + Select All + 全选 + + + + Properties + 属性 + + + + Empty Trash + 清空回收站 + + + + BrowserView + + + No Files + 没有文件 + + + + DesktopView + + + Desktop + 桌面 + + + + FolderModel + + + Cut + 剪切 + + + + Copy + 拷贝 + + + + Undo + 撤销 + + + + Paste + 粘贴 + + + + New Folder + 新建文件夹 + + + + New Documents + 新建文档 + + + + Rename + 重命名 + + + + Move To Trash + 移到回收站 + + + + &Empty Trash + &清空回收站 + + + + Restore from trash + 从回收站恢复 + + + + Delete + 删除 + + + + &Open + &打开 + + + + Select All + 全选 + + + + Change Wallpaper + 更改壁纸 + + + + Properties + 属性 + + + + Set as Wallpaper + 设置为壁纸 + + + + &Properties + &属性 + + + + ItemMenu + + + Open + 打开 + + + + Copy + 拷贝 + + + + Cut + 剪切 + + + + Move to Trash + 移到回收站 + + + + Rename + 重命名 + + + + Open in Terminal + 在终端中打开 + + + + Set As Wallpaper + 设置为壁纸 + + + + Properties + 属性 + + + + PlacesModel + + + Home + 主文件夹 + + + + Desktop + 桌面 + + + + Documents + 文档 + + + + Downloads + 下载 + + + + Music + 音乐 + + + + Pictures + 图片 + + + + Videos + 视频 + + + + Trash + 回收站 + + + + PropertiesDialog + + + Properties + 属性 + + + + Type: + 类型: + + + + Location: + 位置: + + + + Size: + 大小: + + + + Created: + 创建时间: + + + + Modified: + 修改时间: + + + + Accessed: + 访问时间: + + + + Cancel + 取消 + + + + OK + 确定 + + + + main + + + File Manager + 文件管理器 + + +