commit 4f45575c791a73e6711583c680f60954de094666 Author: Konstantinos Sideris Date: Thu Apr 6 02:06:42 2017 +0300 Initial commit diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..ae3158bd --- /dev/null +++ b/.clang-format @@ -0,0 +1,22 @@ +--- +Language: Cpp +AccessModifierOffset: -8 +AlignAfterOpenBracket: true +AlignEscapedNewlinesLeft: false +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +BasedOnStyle: Google +BinPackArguments: false +BinPackParameters: false +BreakBeforeBraces: Linux +BreakConstructorInitializersBeforeComma: true +ColumnLimit: 0 +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ContinuationIndentWidth: 8 +IndentCaseLabels: false +IndentWidth: 8 +MaxEmptyLinesToKeep: 1 +PointerAlignment: Right +UseTab: Always diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..252e1804 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +build +tags + +# C++ objects and libs + +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.dll +*.dylib + +# Qt-es + +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +CMakeLists.txt.user +moc_*.cpp +qrc_*.cpp +ui_*.h +Makefile* +*-build-* + +# QtCreator + +*.autosave + +#QtCtreator Qml +*.qmlproject.user +*.qmlproject.user.* + +#####=== CMake ===##### + +CMakeCache.txt +CMakeFiles +cmake_install.cmake +install_manifest.txt + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..e7d52b2f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,104 @@ +cmake_minimum_required(VERSION 3.1) + +project(nheko CXX) + +set(nheko_VERSION_MAJOR 0) +set(nheko_VERSION_MINOR 1) +set(nheko_VERSION_PATCH 0) + +find_package(Qt5Widgets REQUIRED) +find_package(Qt5Network REQUIRED) + +set(CMAKE_C_COMPILER gcc) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} \ + -std=gnu++11 \ + -Wall \ + -Wextra \ + -Werror \ + -pedantic") + +set(SRC_FILES + src/ChatPage.cc + src/Deserializable.cc + src/HistoryView.cc + src/HistoryViewItem.cc + src/HistoryViewManager.cc + src/InputValidator.cc + src/Login.cc + src/LoginPage.cc + src/MainWindow.cc + src/MatrixClient.cc + src/Profile.cc + src/RoomInfo.cc + src/RoomInfoListItem.cc + src/RoomList.cc + src/RegisterPage.cc + src/SlidingStackWidget.cc + src/Sync.cc + src/TextInputWidget.cc + src/TopRoomBar.cc + src/UserInfoWidget.cc + src/WelcomePage.cc + src/main.cc + + src/ui/Avatar.cc + src/ui/Badge.cc + src/ui/FlatButton.cc + src/ui/RaisedButton.cc + src/ui/Ripple.cc + src/ui/RippleOverlay.cc + src/ui/OverlayWidget.cc + src/ui/TextField.cc + src/ui/Theme.cc + src/ui/ThemeManager.cc +) + +include_directories(include) +include_directories(include/ui) + +qt5_wrap_ui (UI_HEADERS + forms/ChatPage.ui + forms/MainWindow.ui + forms/RoomList.ui +) + +qt5_wrap_cpp(MOC_HEADERS + include/ChatPage.h + include/HistoryView.h + include/HistoryViewItem.h + include/HistoryViewManager.h + include/LoginPage.h + include/MainWindow.h + include/MatrixClient.h + include/RegisterPage.h + include/RoomInfoListItem.h + include/RoomList.h + include/UserInfoWidget.h + include/SlidingStackWidget.h + include/TopRoomBar.h + include/TextInputWidget.h + include/WelcomePage.h + + include/ui/Avatar.h + include/ui/Badge.h + include/ui/FlatButton.h + include/ui/OverlayWidget.h + include/ui/RaisedButton.h + include/ui/Ripple.h + include/ui/RippleOverlay.h + include/ui/TextField.h + include/ui/Theme.h + include/ui/ThemeManager.h +) + +qt5_add_resources(QRC resources/res.qrc) + +add_executable (nheko ${SRC_FILES} ${UI_HEADERS} ${MOC_HEADERS} ${QRC}) +target_link_libraries (nheko Qt5::Widgets Qt5::Network) diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..63c30502 --- /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: + + {{ project }} Copyright (C) {{ year }} {{ organization }} + 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/forms/ChatPage.ui b/forms/ChatPage.ui new file mode 100644 index 00000000..9d981b22 --- /dev/null +++ b/forms/ChatPage.ui @@ -0,0 +1,153 @@ + + + ChatPage + + + + 0 + 0 + 798 + 519 + + + + + 0 + 0 + + + + Form + + + background-color: #171919; + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + 0 + + + 0 + + + + + + 300 + 0 + + + + + 300 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + background-color: #1c3133; +color: #ebebeb; + + + + 0 + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + diff --git a/forms/MainWindow.ui b/forms/MainWindow.ui new file mode 100644 index 00000000..7035b213 --- /dev/null +++ b/forms/MainWindow.ui @@ -0,0 +1,42 @@ + + + MainWindow + + + + 0 + 0 + 850 + 600 + + + + + 0 + 0 + + + + + 850 + 600 + + + + + Open Sans + PreferAntialias + + + + nheko - Matrix Desktop Client + + + background-color: #f9f9f9 + + + + + + + diff --git a/forms/RoomList.ui b/forms/RoomList.ui new file mode 100644 index 00000000..0d65fbf5 --- /dev/null +++ b/forms/RoomList.ui @@ -0,0 +1,157 @@ + + + RoomList + + + + 0 + 0 + 423 + 500 + + + + + 0 + 0 + + + + + 0 + 500 + + + + Form + + + QWidget { +background-color: #171919; +color: #ebebeb; +} + +QScrollBar:vertical { + background-color: #171919; + width: 10px; + border-radius: 20px; + margin: 0px 2px 0 2px; +} + +QScrollBar::handle:vertical { + border-radius: 50px; + background-color: #1c3133; +} + +QScrollBar::add-line:vertical { + border: none; + background: none; +} + +QScrollBar::sub-line:vertical { + border: none; + background: none; +} + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + 0 + 0 + 419 + 496 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + + + + + + + + + + diff --git a/include/ChatPage.h b/include/ChatPage.h new file mode 100644 index 00000000..c3fa6bf6 --- /dev/null +++ b/include/ChatPage.h @@ -0,0 +1,88 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 CHATPAGE_H +#define CHATPAGE_H + +#include +#include +#include +#include +#include + +#include "HistoryViewManager.h" +#include "MatrixClient.h" +#include "RoomInfo.h" +#include "RoomList.h" +#include "TextInputWidget.h" +#include "TopRoomBar.h" +#include "UserInfoWidget.h" + +namespace Ui +{ +class ChatPage; +} + +class ChatPage : public QWidget +{ + Q_OBJECT + +public: + explicit ChatPage(QWidget *parent = 0); + ~ChatPage(); + + // Initialize all the components of the UI. + void bootstrap(QString userid, QString homeserver, QString token); + +public slots: + // Updates the user info box. + void updateOwnProfileInfo(QUrl avatar_url, QString display_name); + void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url); + void initialSyncCompleted(SyncResponse response); + void syncCompleted(SyncResponse response); + void changeTopRoomInfo(const RoomInfo &info); + void sendTextMessage(const QString &msg); + void messageSent(const QString event_id, int txn_id); + void startSync(); + +private: + Ui::ChatPage *ui; + + void setOwnAvatar(QByteArray img); + + RoomList *room_list_; + HistoryViewManager *view_manager_; + + TopRoomBar *top_bar_; + TextInputWidget *text_input_; + + QTimer *sync_timer_; + int sync_interval_; + + RoomInfo current_room_; + QMap room_avatars_; + + UserInfoWidget *user_info_widget_; + + // Matrix client + MatrixClient *matrix_client_; + + // Used for one off media requests. + QNetworkAccessManager *content_downloader_; +}; + +#endif // CHATPAGE_H diff --git a/include/Deserializable.h b/include/Deserializable.h new file mode 100644 index 00000000..6a9b4cd5 --- /dev/null +++ b/include/Deserializable.h @@ -0,0 +1,52 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 DESERIALIZABLE_H +#define DESERIALIZABLE_H + +#include + +#include +#include +#include + +class DeserializationException : public std::exception +{ +public: + explicit DeserializationException(const std::string &msg); + virtual const char *what() const throw(); + +private: + std::string msg_; +}; + +// JSON response structs need to implement the interface. +class Deserializable +{ +public: + virtual void deserialize(QJsonValue) throw(DeserializationException) + { + } + virtual void deserialize(QJsonObject) throw(DeserializationException) + { + } + virtual void deserialize(QJsonDocument) throw(DeserializationException) + { + } +}; + +#endif diff --git a/include/HistoryView.h b/include/HistoryView.h new file mode 100644 index 00000000..9266d6ac --- /dev/null +++ b/include/HistoryView.h @@ -0,0 +1,62 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 HISTORY_VIEW_H +#define HISTORY_VIEW_H + +#include +#include +#include +#include +#include + +#include "HistoryViewItem.h" +#include "Sync.h" + +class HistoryView : public QWidget +{ + Q_OBJECT + +public: + explicit HistoryView(QWidget *parent = 0); + explicit HistoryView(QList events, QWidget *parent = 0); + ~HistoryView(); + + void addHistoryItem(Event event, QString color, bool with_sender); + void addEvents(const QList &events); + +public slots: + void sliderRangeChanged(int min, int max); + +private: + static const QList COLORS; + + void init(); + + QString chooseRandomColor(); + + QVBoxLayout *top_layout_; + QVBoxLayout *scroll_layout_; + + QScrollArea *scroll_area_; + QWidget *scroll_widget_; + + QString last_sender_; + QMap nick_colors_; +}; + +#endif // HISTORY_VIEW_H diff --git a/include/HistoryViewItem.h b/include/HistoryViewItem.h new file mode 100644 index 00000000..b817194b --- /dev/null +++ b/include/HistoryViewItem.h @@ -0,0 +1,41 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 HISTORY_VIEW_ITEM_H +#define HISTORY_VIEW_ITEM_H + +#include +#include +#include + +#include "Sync.h" + +class HistoryViewItem : public QWidget +{ + Q_OBJECT +public: + HistoryViewItem(Event event, bool with_sender, QString color, QWidget *parent = 0); + ~HistoryViewItem(); + +private: + QHBoxLayout *top_layout_; + + QLabel *time_label_; + QLabel *content_label_; +}; + +#endif // HISTORY_VIEW_ITEM_H diff --git a/include/HistoryViewManager.h b/include/HistoryViewManager.h new file mode 100644 index 00000000..8405d005 --- /dev/null +++ b/include/HistoryViewManager.h @@ -0,0 +1,47 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 HISTORY_VIEW_MANAGER_H +#define HISTORY_VIEW_MANAGER_H + +#include +#include +#include + +#include "HistoryView.h" +#include "RoomInfo.h" +#include "Sync.h" + +class HistoryViewManager : public QStackedWidget +{ + Q_OBJECT + +public: + HistoryViewManager(QWidget *parent); + ~HistoryViewManager(); + + void initialize(const Rooms &rooms); + void sync(const Rooms &rooms); + +public slots: + void setHistoryView(const RoomInfo &info); + +private: + QMap views_; +}; + +#endif diff --git a/include/InputValidator.h b/include/InputValidator.h new file mode 100644 index 00000000..feeaf70a --- /dev/null +++ b/include/InputValidator.h @@ -0,0 +1,49 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 MATRIXIDVALIDATOR_H +#define MATRIXIDVALIDATOR_H + +#include +#include + +class InputValidator +{ +public: + InputValidator(QObject *parent = 0); + + // Validators for the different types of input. + QRegExpValidator *id_; + QRegExpValidator *localpart_; + QRegExpValidator *password_; + QRegExpValidator *domain_; + +private: + // Regular expression used to validate the whole matrix id. + const QRegExp matrix_id_; + + // Regular expressino to validate the matrix localpart. + const QRegExp matrix_localpart_; + + // Regular expression to validate a password for a matrix account. + const QRegExp matrix_password_; + + // Regular expression to validate a domain name. + const QRegExp server_domain_; +}; + +#endif // MATRIXIDVALIDATOR_H diff --git a/include/Login.h b/include/Login.h new file mode 100644 index 00000000..857be14b --- /dev/null +++ b/include/Login.h @@ -0,0 +1,56 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 LOGIN_H +#define LOGIN_H + +#include + +#include "Deserializable.h" + +class LoginRequest +{ +public: + LoginRequest(); + LoginRequest(QString username, QString password); + + QByteArray serialize(); + + void setPassword(QString password); + void setUser(QString username); + +private: + QString user_; + QString password_; +}; + +class LoginResponse : public Deserializable +{ +public: + void deserialize(QJsonDocument data) throw(DeserializationException) override; + + QString getAccessToken(); + QString getHomeServer(); + QString getUserId(); + +private: + QString access_token_; + QString home_server_; + QString user_id_; +}; + +#endif // LOGIN_H diff --git a/include/LoginPage.h b/include/LoginPage.h new file mode 100644 index 00000000..d6b57efb --- /dev/null +++ b/include/LoginPage.h @@ -0,0 +1,80 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 LOGINPAGE_H +#define LOGINPAGE_H + +#include +#include +#include +#include + +#include "FlatButton.h" +#include "InputValidator.h" +#include "RaisedButton.h" +#include "TextField.h" + +class LoginPage : public QWidget +{ + Q_OBJECT + +public: + explicit LoginPage(QWidget *parent = 0); + ~LoginPage(); + +signals: + void backButtonClicked(); + + // Emitted after the matrix ID validation. The handler should be + // responsible for performing the actual login with a remote server. + void userLogin(const QString &username, const QString &password, const QString home_server); + +public slots: + // Displays errors produced during the login. + void loginError(QString error_message); + +private slots: + // Callback for the back button. + void onBackButtonClicked(); + + // Callback for the login button. + void onLoginButtonClicked(); + +private: + QVBoxLayout *top_layout_; + + QHBoxLayout *back_layout_; + QHBoxLayout *logo_layout_; + QHBoxLayout *button_layout_; + + QLabel *logo_; + QLabel *error_label_; + + FlatButton *back_button_; + RaisedButton *login_button_; + + QWidget *form_widget_; + QHBoxLayout *form_wrapper_; + QVBoxLayout *form_layout_; + + TextField *username_input_; + TextField *password_input_; + + InputValidator *matrix_id_validator_; +}; + +#endif // LOGINPAGE_H diff --git a/include/MainWindow.h b/include/MainWindow.h new file mode 100644 index 00000000..dbbda3f2 --- /dev/null +++ b/include/MainWindow.h @@ -0,0 +1,84 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include + +#include "ChatPage.h" +#include "LoginPage.h" +#include "MatrixClient.h" +#include "RegisterPage.h" +#include "SlidingStackWidget.h" +#include "WelcomePage.h" + +namespace Ui +{ +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +public slots: + // Show the welcome page in the main window. + void showWelcomePage(); + + // Show the login page in the main window. + void showLoginPage(); + + // Show the register page in the main window. + void showRegisterPage(); + + // Show the chat page and start communicating with the given access token. + void showChatPage(QString user_id, QString home_server, QString token); + + // Performs the actual login. + void matrixLogin(const QString &username, const QString &password, const QString &home_server); + + // Performs the actual registration. + void matrixRegister(const QString &username, const QString &password, const QString &server); + +private: + // The UI component of the main window. + Ui::MainWindow *ui_; + + // The initial welcome screen. + WelcomePage *welcome_page_; + + // The login screen. + LoginPage *login_page_; + + // The register page. + RegisterPage *register_page_; + + // A stacked widget that handles the transitions between widgets. + SlidingStackWidget *sliding_stack_; + + // The main chat area. + ChatPage *chat_page_; + + MatrixClient *matrix_client_; +}; + +#endif // MAINWINDOW_H diff --git a/include/MatrixClient.h b/include/MatrixClient.h new file mode 100644 index 00000000..46d6cc5b --- /dev/null +++ b/include/MatrixClient.h @@ -0,0 +1,134 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 MATRIXCLIENT_H +#define MATRIXCLIENT_H + +#include + +#include "Profile.h" +#include "Sync.h" + +/* + * MatrixClient provides the high level API to communicate with + * a Matrix homeserver. All the responses are returned through signals. + */ +class MatrixClient : public QNetworkAccessManager +{ + Q_OBJECT +public: + MatrixClient(QString server, QObject *parent = 0); + ~MatrixClient(); + + // Client API. + void initialSync(); + void sync(); + void sendTextMessage(QString roomid, QString msg); + void login(const QString &username, const QString &password); + void registerUser(const QString &username, const QString &password); + void versions(); + + inline QString getHomeServer(); + inline void incrementTransactionId(); + +public slots: + // Profile + void getOwnProfile(); + + inline void setServer(QString server); + inline void setAccessToken(QString token); + inline void setNextBatchToken(const QString &next_batch); + +signals: + // Emitted after a error during the login. + void loginError(QString error); + + // Emitted after succesfull user login. A new access token is returned by the server. + void loginSuccess(QString user_id, QString home_server, QString token); + + // Returned profile data for the user's account. + void getOwnProfileResponse(QUrl avatar_url, QString display_name); + void initialSyncCompleted(SyncResponse response); + void syncCompleted(SyncResponse response); + void messageSent(QString event_id, int txn_id); + +private slots: + void onResponse(QNetworkReply *reply); + +private: + enum Endpoint { + GetOwnProfile, + GetProfile, + InitialSync, + Login, + Register, + SendTextMessage, + Sync, + Versions, + }; + + // Response handlers. + void onLoginResponse(QNetworkReply *reply); + void onRegisterResponse(QNetworkReply *reply); + void onVersionsResponse(QNetworkReply *reply); + void onGetOwnProfileResponse(QNetworkReply *reply); + void onSendTextMessageResponse(QNetworkReply *reply); + void onInitialSyncResponse(QNetworkReply *reply); + void onSyncResponse(QNetworkReply *reply); + + // Client API prefix. + QString api_url_; + + // The Matrix server used for communication. + QString server_; + + // The access token used for authentication. + QString token_; + + // Increasing transaction ID. + int txn_id_; + + // Token to be used for the next sync. + QString next_batch_; +}; + +inline QString MatrixClient::getHomeServer() +{ + return server_; +} + +inline void MatrixClient::setServer(QString server) +{ + server_ = "https://" + server; +} + +inline void MatrixClient::setAccessToken(QString token) +{ + token_ = token; +} + +inline void MatrixClient::setNextBatchToken(const QString &next_batch) +{ + next_batch_ = next_batch; +} + +inline void MatrixClient::incrementTransactionId() +{ + txn_id_ += 1; +} + +#endif // MATRIXCLIENT_H diff --git a/include/Profile.h b/include/Profile.h new file mode 100644 index 00000000..a36393ec --- /dev/null +++ b/include/Profile.h @@ -0,0 +1,39 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 PROFILE_H +#define PROFILE_H + +#include +#include + +#include "Deserializable.h" + +class ProfileResponse : public Deserializable +{ +public: + void deserialize(QJsonDocument data) throw(DeserializationException) override; + + QUrl getAvatarUrl(); + QString getDisplayName(); + +private: + QUrl avatar_url_; + QString display_name_; +}; + +#endif // PROFILE_H diff --git a/include/RegisterPage.h b/include/RegisterPage.h new file mode 100644 index 00000000..f1c3848e --- /dev/null +++ b/include/RegisterPage.h @@ -0,0 +1,74 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 REGISTERPAGE_H +#define REGISTERPAGE_H + +#include +#include +#include +#include + +#include "FlatButton.h" +#include "InputValidator.h" +#include "RaisedButton.h" +#include "TextField.h" + +class RegisterPage : public QWidget +{ + Q_OBJECT + +public: + explicit RegisterPage(QWidget *parent = 0); + ~RegisterPage(); + +signals: + void backButtonClicked(); + + // Emitted after successful input validation. The handler should be + // responsible for the actual registering on the remote Matrix server. + void registerUser(const QString &username, const QString &password, const QString &server); + +private slots: + void onBackButtonClicked(); + void onRegisterButtonClicked(); + +private: + QVBoxLayout *top_layout_; + + QHBoxLayout *back_layout_; + QHBoxLayout *logo_layout_; + QHBoxLayout *button_layout_; + + QLabel *logo_; + + FlatButton *back_button_; + RaisedButton *register_button_; + + QWidget *form_widget_; + QHBoxLayout *form_wrapper_; + QVBoxLayout *form_layout_; + + TextField *username_input_; + TextField *password_input_; + TextField *password_confirmation_; + TextField *server_input_; + + InputValidator *validator_; +}; + +#endif // REGISTERPAGE_H diff --git a/include/RoomInfo.h b/include/RoomInfo.h new file mode 100644 index 00000000..9976ba8a --- /dev/null +++ b/include/RoomInfo.h @@ -0,0 +1,49 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 ROOM_INFO_H +#define ROOM_INFO_H + +#include +#include +#include + +class RoomInfo +{ +public: + RoomInfo(); + RoomInfo(QString name, QString topic = "", QUrl avatar_url = QUrl("")); + + QString id() const; + QString name() const; + QString topic() const; + QUrl avatarUrl() const; + + void setAvatarUrl(const QUrl &url); + void setId(const QString &id); + void setName(const QString &name); + void setTopic(const QString &name); + +private: + QString id_; + QString name_; + QString topic_; + QUrl avatar_url_; + QList aliases_; +}; + +#endif // ROOM_INFO_H diff --git a/include/RoomInfoListItem.h b/include/RoomInfoListItem.h new file mode 100644 index 00000000..4eabfa23 --- /dev/null +++ b/include/RoomInfoListItem.h @@ -0,0 +1,93 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 ROOMINFOLISTITEM_H +#define ROOMINFOLISTITEM_H + +#include +#include +#include +#include + +#include "Avatar.h" +#include "RippleOverlay.h" +#include "RoomInfo.h" + +class RoomInfoListItem : public QWidget +{ + Q_OBJECT + +public: + RoomInfoListItem(RoomInfo info, QWidget *parent = 0); + ~RoomInfoListItem(); + + inline bool isPressed(); + inline RoomInfo info(); + inline void setAvatar(const QImage &avatar_image); + +signals: + void clicked(RoomInfo info_); + +public slots: + void setPressedState(bool state); + +protected: + void mousePressEvent(QMouseEvent *event) override; + +private: + void setElidedText(QLabel *label, QString text, int width); + + RippleOverlay *ripple_overlay_; + + RoomInfo info_; + + QHBoxLayout *topLayout_; + + QVBoxLayout *avatarLayout_; + QVBoxLayout *textLayout_; + + QWidget *avatarWidget_; + QWidget *textWidget_; + + QLabel *roomName_; + QLabel *roomTopic_; + + Avatar *roomAvatar_; + + QString pressed_style_; + QString normal_style_; + + bool is_pressed_; + int max_height_; +}; + +inline bool RoomInfoListItem::isPressed() +{ + return is_pressed_; +} + +inline RoomInfo RoomInfoListItem::info() +{ + return info_; +} + +inline void RoomInfoListItem::setAvatar(const QImage &avatar_image) +{ + roomAvatar_->setImage(avatar_image); +} + +#endif // ROOMINFOLISTITEM_H diff --git a/include/RoomList.h b/include/RoomList.h new file mode 100644 index 00000000..d679c785 --- /dev/null +++ b/include/RoomList.h @@ -0,0 +1,60 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 ROOMLIST_H +#define ROOMLIST_H + +#include +#include +#include + +#include "RoomInfo.h" +#include "RoomInfoListItem.h" +#include "Sync.h" + +namespace Ui +{ +class RoomList; +} + +class RoomList : public QWidget +{ + Q_OBJECT + +public: + explicit RoomList(QWidget *parent = 0); + ~RoomList(); + + void appendRoom(QString name); + void setInitialRooms(const Rooms &rooms); + void updateRoomAvatar(const QString &roomid, const QImage &avatar_image); + RoomInfo extractRoomInfo(const State &room_state); + +signals: + void roomChanged(const RoomInfo &info); + void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url); + +public slots: + void highlightSelectedRoom(const RoomInfo &info); + +private: + Ui::RoomList *ui; + + QMap available_rooms_; +}; + +#endif // ROOMLIST_H diff --git a/include/SlidingStackWidget.h b/include/SlidingStackWidget.h new file mode 100644 index 00000000..7686c6eb --- /dev/null +++ b/include/SlidingStackWidget.h @@ -0,0 +1,94 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 SLIDINGSTACKWIDGET_H +#define SLIDINGSTACKWIDGET_H + +#include +#include +#include +#include +#include +#include + +/* + * SlidingStackWidget allows smooth side shifting of widgets, + * in addition to the hard switching from one to another offered + * by QStackedWidget. + */ + +class SlidingStackWidget : public QStackedWidget +{ + Q_OBJECT + +public: + // Defines the animation direction. + enum AnimationDirection { + LEFT_TO_RIGHT, + RIGHT_TO_LEFT, + AUTOMATIC + }; + + SlidingStackWidget(QWidget *parent); + ~SlidingStackWidget(); + +public slots: + // Move to the next widget. + void slideInNext(); + + // Move to the previous widget. + void slideInPrevious(); + + // Move to a widget by index. + void slideInIndex(int index, enum AnimationDirection direction = AnimationDirection::AUTOMATIC); + + int getWidgetIndex(QWidget *widget); +signals: + // Internal signal to alert the engine for the animation's end. + void animationFinished(); + +protected slots: + // Internal slot to handle the end of the animation. + void onAnimationFinished(); + +protected: + // The method that does the main work for the widget transition. + void slideInWidget(QWidget *widget, enum AnimationDirection direction = AnimationDirection::AUTOMATIC); + + // Indicates whether or not the animation is active. + bool active_; + + // The widget currently displayed. + QWidget *window_; + + // The speed of the animation in milliseconds. + int speed_; + + // The animation type. + enum QEasingCurve::Type animation_type_; + + // Current widget's index. + int now_; + + // Reference point. + QPoint current_position_; + + // Next widget's to show index. + int next_; +}; + +#endif // SLIDINGSTACKWIDGET_H diff --git a/include/Sync.h b/include/Sync.h new file mode 100644 index 00000000..110e8a6e --- /dev/null +++ b/include/Sync.h @@ -0,0 +1,119 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 SYNC_H +#define SYNC_H + +#include +#include +#include + +#include "Deserializable.h" + +class Event : public Deserializable +{ +public: + QJsonObject content() const; + QJsonObject unsigned_content() const; + + QString sender() const; + QString state_key() const; + QString type() const; + QString eventId() const; + + uint64_t timestamp() const; + + void deserialize(QJsonValue data) throw(DeserializationException) override; + +private: + QJsonObject content_; + QJsonObject unsigned_; + + QString sender_; + QString state_key_; + QString type_; + QString event_id_; + + uint64_t origin_server_ts_; +}; + +class State : public Deserializable +{ +public: + void deserialize(QJsonValue data) throw(DeserializationException) override; + QList events() const; + +private: + QList events_; +}; + +class Timeline : public Deserializable +{ +public: + QList events() const; + QString previousBatch() const; + bool limited() const; + + void deserialize(QJsonValue data) throw(DeserializationException) override; + +private: + QList events_; + QString prev_batch_; + bool limited_; +}; + +// TODO: Add support for ehpmeral, account_data, undread_notifications +class JoinedRoom : public Deserializable +{ +public: + State state() const; + Timeline timeline() const; + + void deserialize(QJsonValue data) throw(DeserializationException) override; + +private: + State state_; + Timeline timeline_; + /* Ephemeral ephemeral_; */ + /* AccountData account_data_; */ + /* UnreadNotifications unread_notifications_; */ +}; + +// TODO: Add support for invited and left rooms. +class Rooms : public Deserializable +{ +public: + QMap join() const; + void deserialize(QJsonValue data) throw(DeserializationException) override; + +private: + QMap join_; +}; + +class SyncResponse : public Deserializable +{ +public: + void deserialize(QJsonDocument data) throw(DeserializationException) override; + QString nextBatch() const; + Rooms rooms() const; + +private: + QString next_batch_; + Rooms rooms_; +}; + +#endif // SYNC_H diff --git a/include/TextInputWidget.h b/include/TextInputWidget.h new file mode 100644 index 00000000..35a12892 --- /dev/null +++ b/include/TextInputWidget.h @@ -0,0 +1,53 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 TEXT_INPUT_WIDGET_H +#define TEXT_INPUT_WIDGET_H + +#include +#include +#include +#include + +#include "FlatButton.h" + +class TextInputWidget : public QWidget +{ + Q_OBJECT + +public: + TextInputWidget(QWidget *parent = 0); + ~TextInputWidget(); + +public slots: + void onSendButtonClicked(); + +signals: + void sendTextMessage(QString msg); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QHBoxLayout *top_layout_; + QLineEdit *input_; + + FlatButton *send_file_button_; + FlatButton *send_message_button_; +}; + +#endif // TEXT_INPUT_WIDGET_H diff --git a/include/TopRoomBar.h b/include/TopRoomBar.h new file mode 100644 index 00000000..247cb9a2 --- /dev/null +++ b/include/TopRoomBar.h @@ -0,0 +1,79 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 TOP_ROOM_BAR_H +#define TOP_ROOM_BAR_H + +#include +#include +#include +#include +#include +#include + +#include "Avatar.h" +#include "FlatButton.h" + +class TopRoomBar : public QWidget +{ + Q_OBJECT +public: + TopRoomBar(QWidget *parent = 0); + ~TopRoomBar(); + + inline void updateRoomAvatar(const QImage &avatar_image); + inline void updateRoomAvatar(const QIcon &icon); + inline void updateRoomName(const QString &name); + inline void updateRoomTopic(const QString &topic); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QHBoxLayout *top_layout_; + QVBoxLayout *text_layout_; + + QLabel *name_label_; + QLabel *topic_label_; + + FlatButton *search_button_; + FlatButton *settings_button_; + + Avatar *avatar_; +}; + +inline void TopRoomBar::updateRoomAvatar(const QImage &avatar_image) +{ + avatar_->setImage(avatar_image); +} + +inline void TopRoomBar::updateRoomAvatar(const QIcon &icon) +{ + avatar_->setIcon(icon); +} + +inline void TopRoomBar::updateRoomName(const QString &name) +{ + name_label_->setText(name); +} + +inline void TopRoomBar::updateRoomTopic(const QString &topic) +{ + topic_label_->setText(topic); +} + +#endif // TOP_ROOM_BAR_H diff --git a/include/UserInfoWidget.h b/include/UserInfoWidget.h new file mode 100644 index 00000000..8cd4b765 --- /dev/null +++ b/include/UserInfoWidget.h @@ -0,0 +1,60 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 USER_INFO_WIDGET_H +#define USER_INFO_WIDGET_H + +#include +#include +#include +#include + +#include "Avatar.h" +#include "FlatButton.h" + +class UserInfoWidget : public QWidget +{ + Q_OBJECT + +public: + UserInfoWidget(QWidget *parent = 0); + ~UserInfoWidget(); + + void setAvatar(const QImage &img); + void setDisplayName(const QString &name); + void setUserId(const QString &userid); + +private: + Avatar *userAvatar_; + + QHBoxLayout *topLayout_; + QHBoxLayout *avatarLayout_; + QVBoxLayout *textLayout_; + QHBoxLayout *buttonLayout_; + + FlatButton *settingsButton_; + + QLabel *displayNameLabel_; + QLabel *userIdLabel_; + + QString display_name_; + QString userid_; + + QImage avatar_image_; +}; + +#endif diff --git a/include/WelcomePage.h b/include/WelcomePage.h new file mode 100644 index 00000000..3cd6e664 --- /dev/null +++ b/include/WelcomePage.h @@ -0,0 +1,61 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 WELCOMEPAGE_H +#define WELCOMEPAGE_H + +#include +#include +#include +#include +#include + +#include "RaisedButton.h" + +class WelcomePage : public QWidget +{ + Q_OBJECT + +public: + explicit WelcomePage(QWidget *parent = 0); + ~WelcomePage(); + +signals: + // Notify that the user wants to login in. + void userLogin(); + + // Notify that the user wants to register. + void userRegister(); + +private slots: + void onLoginButtonClicked(); + void onRegisterButtonClicked(); + +private: + QVBoxLayout *top_layout_; + QHBoxLayout *button_layout_; + + QLabel *intro_banner_; + QLabel *intro_text_; + + QSpacerItem *button_spacer_; + + RaisedButton *register_button_; + RaisedButton *login_button_; +}; + +#endif // WELCOMEPAGE_H diff --git a/include/ui/Avatar.h b/include/ui/Avatar.h new file mode 100644 index 00000000..afbf6aad --- /dev/null +++ b/include/ui/Avatar.h @@ -0,0 +1,51 @@ +#ifndef UI_AVATAR_H +#define UI_AVATAR_H + +#include +#include +#include +#include + +#include "Theme.h" + +class Avatar : public QWidget +{ + Q_OBJECT + + Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) + Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) + +public: + explicit Avatar(QWidget *parent = 0); + ~Avatar(); + + void setBackgroundColor(const QColor &color); + void setIcon(const QIcon &icon); + void setImage(const QImage &image); + void setLetter(const QChar &letter); + void setSize(int size); + void setTextColor(const QColor &color); + + QColor backgroundColor() const; + QColor textColor() const; + int size() const; + + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + void init(); + + ui::AvatarType type_; + QChar letter_; + QColor background_color_; + QColor text_color_; + QIcon icon_; + QImage image_; + QPixmap pixmap_; + int size_; +}; + +#endif // UI_AVATAR_H diff --git a/include/ui/Badge.h b/include/ui/Badge.h new file mode 100644 index 00000000..774b03ad --- /dev/null +++ b/include/ui/Badge.h @@ -0,0 +1,63 @@ +#ifndef UI_BADGE_H +#define UI_BADGE_H + +#include +#include +#include +#include + +#include "OverlayWidget.h" + +class Badge : public OverlayWidget +{ + Q_OBJECT + + Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) + Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) + Q_PROPERTY(QPointF relativePosition WRITE setRelativePosition READ relativePosition) + +public: + explicit Badge(QWidget *parent = 0); + explicit Badge(const QIcon &icon, QWidget *parent = 0); + explicit Badge(const QString &text, QWidget *parent = 0); + ~Badge(); + + void setBackgroundColor(const QColor &color); + void setTextColor(const QColor &color); + void setIcon(const QIcon &icon); + void setRelativePosition(const QPointF &pos); + void setRelativePosition(qreal x, qreal y); + void setRelativeXPosition(qreal x); + void setRelativeYPosition(qreal y); + void setText(const QString &text); + + QIcon icon() const; + QString text() const; + QColor backgroundColor() const; + QColor textColor() const; + QPointF relativePosition() const; + QSize sizeHint() const override; + qreal relativeXPosition() const; + qreal relativeYPosition() const; + +protected: + void paintEvent(QPaintEvent *event) override; + int getDiameter() const; + +private: + void init(); + + QColor background_color_; + QColor text_color_; + + QIcon icon_; + QSize size_; + QString text_; + + int padding_; + + qreal x_; + qreal y_; +}; + +#endif // UI_BADGE_H diff --git a/include/ui/FlatButton.h b/include/ui/FlatButton.h new file mode 100644 index 00000000..047890c7 --- /dev/null +++ b/include/ui/FlatButton.h @@ -0,0 +1,210 @@ +#ifndef UI_FLAT_BUTTON_H +#define UI_FLAT_BUTTON_H + +#include +#include +#include +#include +#include + +#include "RippleOverlay.h" +#include "Theme.h" + +class FlatButton; + +class FlatButtonStateMachine : public QStateMachine +{ + Q_OBJECT + + Q_PROPERTY(qreal overlayOpacity WRITE setOverlayOpacity READ overlayOpacity) + Q_PROPERTY(qreal checkedOverlayProgress WRITE setCheckedOverlayProgress READ checkedOverlayProgress) + Q_PROPERTY(qreal haloOpacity WRITE setHaloOpacity READ haloOpacity) + Q_PROPERTY(qreal haloSize WRITE setHaloSize READ haloSize) + Q_PROPERTY(qreal haloScaleFactor WRITE setHaloScaleFactor READ haloScaleFactor) + +public: + explicit FlatButtonStateMachine(FlatButton *parent); + ~FlatButtonStateMachine(); + + void setOverlayOpacity(qreal opacity); + void setCheckedOverlayProgress(qreal opacity); + void setHaloOpacity(qreal opacity); + void setHaloSize(qreal size); + void setHaloScaleFactor(qreal factor); + + inline qreal overlayOpacity() const; + inline qreal checkedOverlayProgress() const; + inline qreal haloOpacity() const; + inline qreal haloSize() const; + inline qreal haloScaleFactor() const; + + void startAnimations(); + void setupProperties(); + void updateCheckedStatus(); + +signals: + void buttonPressed(); + void buttonChecked(); + void buttonUnchecked(); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + void addTransition(QObject *object, const char *signal, QState *fromState, QState *toState); + void addTransition(QObject *object, QEvent::Type eventType, QState *fromState, QState *toState); + void addTransition(QAbstractTransition *transition, QState *fromState, QState *toState); + + FlatButton *const button_; + + QState *const top_level_state_; + QState *const config_state_; + QState *const checkable_state_; + QState *const checked_state_; + QState *const unchecked_state_; + QState *const neutral_state_; + QState *const neutral_focused_state_; + QState *const hovered_state_; + QState *const hovered_focused_state_; + QState *const pressed_state_; + + QSequentialAnimationGroup *const halo_animation_; + + qreal overlay_opacity_; + qreal checked_overlay_progress_; + qreal halo_opacity_; + qreal halo_size_; + qreal halo_scale_factor_; + + bool was_checked_; +}; + +inline qreal FlatButtonStateMachine::overlayOpacity() const +{ + return overlay_opacity_; +} + +inline qreal FlatButtonStateMachine::checkedOverlayProgress() const +{ + return checked_overlay_progress_; +} + +inline qreal FlatButtonStateMachine::haloOpacity() const +{ + return halo_opacity_; +} + +inline qreal FlatButtonStateMachine::haloSize() const +{ + return halo_size_; +} + +inline qreal FlatButtonStateMachine::haloScaleFactor() const +{ + return halo_scale_factor_; +} + +class FlatButton : public QPushButton +{ + Q_OBJECT + + Q_PROPERTY(QColor foregroundColor WRITE setForegroundColor READ foregroundColor) + Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) + Q_PROPERTY(QColor overlayColor WRITE setOverlayColor READ overlayColor) + Q_PROPERTY(QColor disabledForegroundColor WRITE setDisabledForegroundColor READ disabledForegroundColor) + Q_PROPERTY(QColor disabledBackgroundColor WRITE setDisabledBackgroundColor READ disabledBackgroundColor) + Q_PROPERTY(qreal fontSize WRITE setFontSize READ fontSize) + +public: + explicit FlatButton(QWidget *parent = 0, ui::ButtonPreset preset = ui::FlatPreset); + explicit FlatButton(const QString &text, QWidget *parent = 0, ui::ButtonPreset preset = ui::FlatPreset); + FlatButton(const QString &text, ui::Role role, QWidget *parent = 0, ui::ButtonPreset preset = ui::FlatPreset); + ~FlatButton(); + + void applyPreset(ui::ButtonPreset preset); + + void setBackgroundColor(const QColor &color); + void setBackgroundMode(Qt::BGMode mode); + void setBaseOpacity(qreal opacity); + void setCheckable(bool value); + void setCornerRadius(qreal radius); + void setDisabledBackgroundColor(const QColor &color); + void setDisabledForegroundColor(const QColor &color); + void setFixedRippleRadius(qreal radius); + void setFontSize(qreal size); + void setForegroundColor(const QColor &color); + void setHaloVisible(bool visible); + void setHasFixedRippleRadius(bool value); + void setIconPlacement(ui::ButtonIconPlacement placement); + void setOverlayColor(const QColor &color); + void setOverlayStyle(ui::OverlayStyle style); + void setRippleStyle(ui::RippleStyle style); + void setRole(ui::Role role); + + QColor foregroundColor() const; + QColor backgroundColor() const; + QColor overlayColor() const; + QColor disabledForegroundColor() const; + QColor disabledBackgroundColor() const; + + qreal fontSize() const; + qreal cornerRadius() const; + qreal baseOpacity() const; + + bool isHaloVisible() const; + bool hasFixedRippleRadius() const; + + ui::Role role() const; + ui::OverlayStyle overlayStyle() const; + ui::RippleStyle rippleStyle() const; + ui::ButtonIconPlacement iconPlacement() const; + + Qt::BGMode backgroundMode() const; + + QSize sizeHint() const override; + +protected: + enum { + IconPadding = 12 + }; + + void checkStateSet() override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void paintEvent(QPaintEvent *event) override; + + virtual void paintBackground(QPainter *painter); + virtual void paintHalo(QPainter *painter); + virtual void paintForeground(QPainter *painter); + virtual void updateClipPath(); + + void init(); + +private: + RippleOverlay *ripple_overlay_; + FlatButtonStateMachine *state_machine_; + + ui::Role role_; + ui::RippleStyle ripple_style_; + ui::ButtonIconPlacement icon_placement_; + ui::OverlayStyle overlay_style_; + + Qt::BGMode bg_mode_; + + QColor background_color_; + QColor foreground_color_; + QColor overlay_color_; + QColor disabled_color_; + QColor disabled_background_color_; + + qreal fixed_ripple_radius_; + qreal corner_radius_; + qreal base_opacity_; + qreal font_size_; + + bool use_fixed_ripple_radius_; + bool halo_visible_; +}; + +#endif // UI_FLAT_BUTTON_H diff --git a/include/ui/OverlayWidget.h b/include/ui/OverlayWidget.h new file mode 100644 index 00000000..020393ad --- /dev/null +++ b/include/ui/OverlayWidget.h @@ -0,0 +1,23 @@ +#ifndef UI_OVERLAY_WIDGET_H +#define UI_OVERLAY_WIDGET_H + +#include +#include +#include + +class OverlayWidget : public QWidget +{ + Q_OBJECT + +public: + explicit OverlayWidget(QWidget *parent = 0); + ~OverlayWidget(); + +protected: + bool event(QEvent *event) override; + bool eventFilter(QObject *obj, QEvent *event) override; + + QRect overlayGeometry() const; +}; + +#endif // UI_OVERLAY_WIDGET_H diff --git a/include/ui/RaisedButton.h b/include/ui/RaisedButton.h new file mode 100644 index 00000000..7a46173f --- /dev/null +++ b/include/ui/RaisedButton.h @@ -0,0 +1,31 @@ +#ifndef UI_RAISED_BUTTON_H +#define UI_RAISED_BUTTON_H + +#include +#include +#include + +#include "FlatButton.h" + +class RaisedButton : public FlatButton +{ + Q_OBJECT + +public: + explicit RaisedButton(QWidget *parent = 0); + explicit RaisedButton(const QString &text, QWidget *parent = 0); + ~RaisedButton(); + +protected: + bool event(QEvent *event) override; + +private: + void init(); + + QStateMachine *shadow_state_machine_; + QState *normal_state_; + QState *pressed_state_; + QGraphicsDropShadowEffect *effect_; +}; + +#endif // UI_RAISED_BUTTON_H diff --git a/include/ui/Ripple.h b/include/ui/Ripple.h new file mode 100644 index 00000000..a66a583e --- /dev/null +++ b/include/ui/Ripple.h @@ -0,0 +1,136 @@ +#ifndef UI_RIPPLE_H +#define UI_RIPPLE_H + +#include +#include +#include +#include +#include + +class RippleOverlay; + +class Ripple : public QParallelAnimationGroup +{ + Q_OBJECT + + Q_PROPERTY(qreal radius WRITE setRadius READ radius) + Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity) + +public: + explicit Ripple(const QPoint ¢er, QObject *parent = 0); + Ripple(const QPoint ¢er, RippleOverlay *overlay, QObject *parent = 0); + ~Ripple(); + + inline void setOverlay(RippleOverlay *overlay); + + void setRadius(qreal radius); + void setOpacity(qreal opacity); + void setColor(const QColor &color); + void setBrush(const QBrush &brush); + + inline qreal radius() const; + inline qreal opacity() const; + inline QColor color() const; + inline QBrush brush() const; + inline QPoint center() const; + + inline QPropertyAnimation *radiusAnimation() const; + inline QPropertyAnimation *opacityAnimation() const; + + inline void setOpacityStartValue(qreal value); + inline void setOpacityEndValue(qreal value); + inline void setRadiusStartValue(qreal value); + inline void setRadiusEndValue(qreal value); + inline void setDuration(int msecs); + +protected slots: + void destroy(); + +private: + Q_DISABLE_COPY(Ripple) + + QPropertyAnimation *animate(const QByteArray &property, + const QEasingCurve &easing = QEasingCurve::OutQuad, + int duration = 800); + + void init(); + + RippleOverlay *overlay_; + + QPropertyAnimation *const radius_anim_; + QPropertyAnimation *const opacity_anim_; + + qreal radius_; + qreal opacity_; + + QPoint center_; + QBrush brush_; +}; + +inline void Ripple::setOverlay(RippleOverlay *overlay) +{ + overlay_ = overlay; +} + +inline qreal Ripple::radius() const +{ + return radius_; +} + +inline qreal Ripple::opacity() const +{ + return opacity_; +} + +inline QColor Ripple::color() const +{ + return brush_.color(); +} + +inline QBrush Ripple::brush() const +{ + return brush_; +} + +inline QPoint Ripple::center() const +{ + return center_; +} + +inline QPropertyAnimation *Ripple::radiusAnimation() const +{ + return radius_anim_; +} + +inline QPropertyAnimation *Ripple::opacityAnimation() const +{ + return opacity_anim_; +} + +inline void Ripple::setOpacityStartValue(qreal value) +{ + opacity_anim_->setStartValue(value); +} + +inline void Ripple::setOpacityEndValue(qreal value) +{ + opacity_anim_->setEndValue(value); +} + +inline void Ripple::setRadiusStartValue(qreal value) +{ + radius_anim_->setStartValue(value); +} + +inline void Ripple::setRadiusEndValue(qreal value) +{ + radius_anim_->setEndValue(value); +} + +inline void Ripple::setDuration(int msecs) +{ + radius_anim_->setDuration(msecs); + opacity_anim_->setDuration(msecs); +} + +#endif // UI_RIPPLE_H diff --git a/include/ui/RippleOverlay.h b/include/ui/RippleOverlay.h new file mode 100644 index 00000000..54398efa --- /dev/null +++ b/include/ui/RippleOverlay.h @@ -0,0 +1,58 @@ +#ifndef UI_RIPPLE_OVERLAY_H +#define UI_RIPPLE_OVERLAY_H + +#include + +#include "OverlayWidget.h" + +class Ripple; + +class RippleOverlay : public OverlayWidget +{ + Q_OBJECT + +public: + explicit RippleOverlay(QWidget *parent = 0); + ~RippleOverlay(); + + void addRipple(Ripple *ripple); + void addRipple(const QPoint &position, qreal radius = 300); + + void removeRipple(Ripple *ripple); + + inline void setClipping(bool enable); + inline bool hasClipping() const; + + inline void setClipPath(const QPainterPath &path); + +protected: + void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; + +private: + Q_DISABLE_COPY(RippleOverlay) + + void paintRipple(QPainter *painter, Ripple *ripple); + + QList ripples_; + QPainterPath clip_path_; + bool use_clip_; +}; + +inline void RippleOverlay::setClipping(bool enable) +{ + use_clip_ = enable; + update(); +} + +inline bool RippleOverlay::hasClipping() const +{ + return use_clip_; +} + +inline void RippleOverlay::setClipPath(const QPainterPath &path) +{ + clip_path_ = path; + update(); +} + +#endif // UI_RIPPLE_OVERLAY_H diff --git a/include/ui/TextField.h b/include/ui/TextField.h new file mode 100644 index 00000000..953c8f29 --- /dev/null +++ b/include/ui/TextField.h @@ -0,0 +1,170 @@ +#ifndef UI_TEXT_FIELD_H +#define UI_TEXT_FIELD_H + +#include +#include +#include +#include +#include +#include + +class TextField; +class TextFieldLabel; +class TextFieldStateMachine; + +class TextField : public QLineEdit +{ + Q_OBJECT + + Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) + Q_PROPERTY(QColor inkColor WRITE setInkColor READ inkColor) + Q_PROPERTY(QColor underlineColor WRITE setUnderlineColor READ underlineColor) + +public: + explicit TextField(QWidget *parent = 0); + ~TextField(); + + void setInkColor(const QColor &color); + void setBackgroundColor(const QColor &color); + void setLabel(const QString &label); + void setLabelColor(const QColor &color); + void setLabelFontSize(qreal size); + void setShowLabel(bool value); + void setTextColor(const QColor &color); + void setUnderlineColor(const QColor &color); + + QColor inkColor() const; + QColor labelColor() const; + QColor textColor() const; + QColor underlineColor() const; + QColor backgroundColor() const; + QString label() const; + bool hasLabel() const; + qreal labelFontSize() const; + +protected: + bool event(QEvent *event) override; + void paintEvent(QPaintEvent *event) override; + +private: + void init(); + + QColor ink_color_; + QColor background_color_; + QColor label_color_; + QColor text_color_; + QColor underline_color_; + QString label_text_; + TextFieldLabel *label_; + TextFieldStateMachine *state_machine_; + bool show_label_; + qreal label_font_size_; +}; + +class TextFieldLabel : public QWidget +{ + Q_OBJECT + + Q_PROPERTY(qreal scale WRITE setScale READ scale) + Q_PROPERTY(QPointF offset WRITE setOffset READ offset) + Q_PROPERTY(QColor color WRITE setColor READ color) + +public: + TextFieldLabel(TextField *parent); + ~TextFieldLabel(); + + inline void setColor(const QColor &color); + inline void setOffset(const QPointF &pos); + inline void setScale(qreal scale); + + inline QColor color() const; + inline QPointF offset() const; + inline qreal scale() const; + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + TextField *const text_field_; + + QColor color_; + qreal scale_; + qreal x_; + qreal y_; +}; + +inline void TextFieldLabel::setColor(const QColor &color) +{ + color_ = color; + update(); +} + +inline void TextFieldLabel::setOffset(const QPointF &pos) +{ + x_ = pos.x(); + y_ = pos.y(); + update(); +} + +inline void TextFieldLabel::setScale(qreal scale) +{ + scale_ = scale; + update(); +} + +inline QPointF TextFieldLabel::offset() const +{ + return QPointF(x_, y_); +} +inline qreal TextFieldLabel::scale() const +{ + return scale_; +} +inline QColor TextFieldLabel::color() const +{ + return color_; +} + +class TextFieldStateMachine : public QStateMachine +{ + Q_OBJECT + + Q_PROPERTY(qreal progress WRITE setProgress READ progress) + +public: + TextFieldStateMachine(TextField *parent); + ~TextFieldStateMachine(); + + inline void setProgress(qreal progress); + void setLabel(TextFieldLabel *label); + + inline qreal progress() const; + +public slots: + void setupProperties(); + +private: + QPropertyAnimation *color_anim_; + QPropertyAnimation *offset_anim_; + + QState *focused_state_; + QState *normal_state_; + + TextField *text_field_; + TextFieldLabel *label_; + + qreal progress_; +}; + +inline void TextFieldStateMachine::setProgress(qreal progress) +{ + progress_ = progress; + text_field_->update(); +} + +inline qreal TextFieldStateMachine::progress() const +{ + return progress_; +} + +#endif // UI_TEXT_FIELD_H diff --git a/include/ui/Theme.h b/include/ui/Theme.h new file mode 100644 index 00000000..41739a98 --- /dev/null +++ b/include/ui/Theme.h @@ -0,0 +1,89 @@ +#ifndef UI_THEME_H +#define UI_THEME_H + +#include +#include +#include + +namespace ui +{ +enum AvatarType { + Icon, + Image, + Letter +}; + +// Default font size. +const int FontSize = 16; + +// Default avatar size. Width and height. +const int AvatarSize = 40; + +enum ButtonPreset { + FlatPreset, + CheckablePreset +}; + +enum RippleStyle { + CenteredRipple, + PositionedRipple, + NoRipple +}; + +enum OverlayStyle { + NoOverlay, + TintedOverlay, + GrayOverlay +}; + +enum Role { + Default, + Primary, + Secondary +}; + +enum ButtonIconPlacement { + LeftIcon, + RightIcon +}; + +enum ProgressType { + DeterminateProgress, + IndeterminateProgress +}; + +enum Color { + Black, + BrightWhite, + FadedWhite, + MediumWhite, + DarkGreen, + LightGreen, + BrightGreen, + Gray, + Red, + Blue, + Transparent +}; + +} // namespace ui + +class Theme : public QObject +{ + Q_OBJECT +public: + explicit Theme(QObject *parent = 0); + ~Theme(); + + QColor getColor(const QString &key) const; + + void setColor(const QString &key, const QColor &color); + void setColor(const QString &key, ui::Color &color); + +private: + QColor rgba(int r, int g, int b, qreal a) const; + + QHash colors_; +}; + +#endif // UI_THEME_H diff --git a/include/ui/ThemeManager.h b/include/ui/ThemeManager.h new file mode 100644 index 00000000..426d71ec --- /dev/null +++ b/include/ui/ThemeManager.h @@ -0,0 +1,33 @@ +#ifndef UI_THEME_MANAGER_H +#define UI_THEME_MANAGER_H + +#include + +#include "Theme.h" + +class ThemeManager : public QCommonStyle +{ + Q_OBJECT + +public: + inline static ThemeManager &instance(); + + void setTheme(Theme *theme); + QColor themeColor(const QString &key) const; + +private: + ThemeManager(); + + ThemeManager(ThemeManager const &); + void operator=(ThemeManager const &); + + Theme *theme_; +}; + +inline ThemeManager &ThemeManager::instance() +{ + static ThemeManager instance; + return instance; +} + +#endif // UI_THEME_MANAGER_H diff --git a/resources/fonts/LICENSE.txt b/resources/fonts/LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/resources/fonts/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/resources/fonts/OpenSans-Bold.ttf b/resources/fonts/OpenSans-Bold.ttf new file mode 100644 index 00000000..fd79d43b Binary files /dev/null and b/resources/fonts/OpenSans-Bold.ttf differ diff --git a/resources/fonts/OpenSans-BoldItalic.ttf b/resources/fonts/OpenSans-BoldItalic.ttf new file mode 100644 index 00000000..9bc80095 Binary files /dev/null and b/resources/fonts/OpenSans-BoldItalic.ttf differ diff --git a/resources/fonts/OpenSans-ExtraBold.ttf b/resources/fonts/OpenSans-ExtraBold.ttf new file mode 100644 index 00000000..21f6f84a Binary files /dev/null and b/resources/fonts/OpenSans-ExtraBold.ttf differ diff --git a/resources/fonts/OpenSans-ExtraBoldItalic.ttf b/resources/fonts/OpenSans-ExtraBoldItalic.ttf new file mode 100644 index 00000000..31cb6883 Binary files /dev/null and b/resources/fonts/OpenSans-ExtraBoldItalic.ttf differ diff --git a/resources/fonts/OpenSans-Italic.ttf b/resources/fonts/OpenSans-Italic.ttf new file mode 100644 index 00000000..c90da48f Binary files /dev/null and b/resources/fonts/OpenSans-Italic.ttf differ diff --git a/resources/fonts/OpenSans-Light.ttf b/resources/fonts/OpenSans-Light.ttf new file mode 100644 index 00000000..0d381897 Binary files /dev/null and b/resources/fonts/OpenSans-Light.ttf differ diff --git a/resources/fonts/OpenSans-LightItalic.ttf b/resources/fonts/OpenSans-LightItalic.ttf new file mode 100644 index 00000000..68299c4b Binary files /dev/null and b/resources/fonts/OpenSans-LightItalic.ttf differ diff --git a/resources/fonts/OpenSans-Regular.ttf b/resources/fonts/OpenSans-Regular.ttf new file mode 100644 index 00000000..db433349 Binary files /dev/null and b/resources/fonts/OpenSans-Regular.ttf differ diff --git a/resources/fonts/OpenSans-Semibold.ttf b/resources/fonts/OpenSans-Semibold.ttf new file mode 100644 index 00000000..1a7679e3 Binary files /dev/null and b/resources/fonts/OpenSans-Semibold.ttf differ diff --git a/resources/fonts/OpenSans-SemiboldItalic.ttf b/resources/fonts/OpenSans-SemiboldItalic.ttf new file mode 100644 index 00000000..59b6d16b Binary files /dev/null and b/resources/fonts/OpenSans-SemiboldItalic.ttf differ diff --git a/resources/icons/add-file.png b/resources/icons/add-file.png new file mode 100644 index 00000000..806b166d Binary files /dev/null and b/resources/icons/add-file.png differ diff --git a/resources/icons/clip-dark.png b/resources/icons/clip-dark.png new file mode 100644 index 00000000..c3c34fac Binary files /dev/null and b/resources/icons/clip-dark.png differ diff --git a/resources/icons/cog.png b/resources/icons/cog.png new file mode 100644 index 00000000..84311166 Binary files /dev/null and b/resources/icons/cog.png differ diff --git a/resources/icons/left-angle.png b/resources/icons/left-angle.png new file mode 100644 index 00000000..71543286 Binary files /dev/null and b/resources/icons/left-angle.png differ diff --git a/resources/icons/left-chevron.png b/resources/icons/left-chevron.png new file mode 100644 index 00000000..386395fb Binary files /dev/null and b/resources/icons/left-chevron.png differ diff --git a/resources/icons/plus-symbol.png b/resources/icons/plus-symbol.png new file mode 100644 index 00000000..35aef7d0 Binary files /dev/null and b/resources/icons/plus-symbol.png differ diff --git a/resources/icons/search.png b/resources/icons/search.png new file mode 100644 index 00000000..94e2f4fc Binary files /dev/null and b/resources/icons/search.png differ diff --git a/resources/icons/send-button.png b/resources/icons/send-button.png new file mode 100644 index 00000000..d19bf69f Binary files /dev/null and b/resources/icons/send-button.png differ diff --git a/resources/icons/share-dark.png b/resources/icons/share-dark.png new file mode 100644 index 00000000..3e4d42a8 Binary files /dev/null and b/resources/icons/share-dark.png differ diff --git a/resources/icons/user-shape.png b/resources/icons/user-shape.png new file mode 100644 index 00000000..1b4b46cc Binary files /dev/null and b/resources/icons/user-shape.png differ diff --git a/resources/res.qrc b/resources/res.qrc new file mode 100644 index 00000000..a6e4580a --- /dev/null +++ b/resources/res.qrc @@ -0,0 +1,27 @@ + + + icons/left-chevron.png + icons/left-angle.png + icons/add-file.png + icons/send-button.png + icons/cog.png + icons/search.png + icons/plus-symbol.png + icons/clip-dark.png + icons/share-dark.png + icons/user-shape.png + + + + fonts/OpenSans-Light.ttf + fonts/OpenSans-LightItalic.ttf + fonts/OpenSans-Regular.ttf + fonts/OpenSans-Italic.ttf + fonts/OpenSans-Bold.ttf + fonts/OpenSans-BoldItalic.ttf + fonts/OpenSans-Semibold.ttf + fonts/OpenSans-SemiboldItalic.ttf + fonts/OpenSans-ExtraBold.ttf + fonts/OpenSans-ExtraBoldItalic.ttf + + diff --git a/src/ChatPage.cc b/src/ChatPage.cc new file mode 100644 index 00000000..a17e8c65 --- /dev/null +++ b/src/ChatPage.cc @@ -0,0 +1,277 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "ui_ChatPage.h" + +#include +#include +#include +#include +#include +#include + +#include "ChatPage.h" +#include "Sync.h" +#include "UserInfoWidget.h" + +ChatPage::ChatPage(QWidget *parent) + : QWidget(parent) + , ui(new Ui::ChatPage) + , sync_interval_(2000) +{ + ui->setupUi(this); + matrix_client_ = new MatrixClient("matrix.org", parent); + content_downloader_ = new QNetworkAccessManager(parent); + + room_list_ = new RoomList(this); + + top_bar_ = new TopRoomBar(this); + ui->topBarLayout->addWidget(top_bar_); + + view_manager_ = new HistoryViewManager(this); + ui->mainContentLayout->addWidget(view_manager_); + + text_input_ = new TextInputWidget(this); + ui->contentLayout->addWidget(text_input_); + + user_info_widget_ = new UserInfoWidget(ui->sideBarTopWidget); + + connect(room_list_, + SIGNAL(roomChanged(const RoomInfo &)), + this, + SLOT(changeTopRoomInfo(const RoomInfo &))); + + connect(room_list_, + SIGNAL(roomChanged(const RoomInfo &)), + view_manager_, + SLOT(setHistoryView(const RoomInfo &))); + + connect(room_list_, + SIGNAL(fetchRoomAvatar(const QString &, const QUrl &)), + this, + SLOT(fetchRoomAvatar(const QString &, const QUrl &))); + + connect(text_input_, + SIGNAL(sendTextMessage(const QString &)), + this, + SLOT(sendTextMessage(const QString &))); + + ui->sideBarTopUserInfoLayout->addWidget(user_info_widget_); + ui->sideBarMainLayout->addWidget(room_list_); + + connect(matrix_client_, + SIGNAL(initialSyncCompleted(SyncResponse)), + this, + SLOT(initialSyncCompleted(SyncResponse))); + connect(matrix_client_, + SIGNAL(syncCompleted(SyncResponse)), + this, + SLOT(syncCompleted(SyncResponse))); + connect(matrix_client_, + SIGNAL(getOwnProfileResponse(QUrl, QString)), + this, + SLOT(updateOwnProfileInfo(QUrl, QString))); + connect(matrix_client_, + SIGNAL(messageSent(QString, int)), + this, + SLOT(messageSent(QString, int))); +} + +void ChatPage::messageSent(QString event_id, int txn_id) +{ + Q_UNUSED(event_id); + + QSettings settings; + settings.setValue("client/transaction_id", txn_id + 1); +} + +void ChatPage::sendTextMessage(const QString &msg) +{ + auto room = current_room_; + matrix_client_->sendTextMessage(current_room_.id(), msg); +} + +void ChatPage::bootstrap(QString userid, QString homeserver, QString token) +{ + Q_UNUSED(userid); + + matrix_client_->setServer(homeserver); + matrix_client_->setAccessToken(token); + + matrix_client_->getOwnProfile(); + matrix_client_->initialSync(); +} + +void ChatPage::startSync() +{ + matrix_client_->sync(); +} + +void ChatPage::setOwnAvatar(QByteArray img) +{ + if (img.size() == 0) + return; + + QPixmap pixmap; + pixmap.loadFromData(img); + user_info_widget_->setAvatar(pixmap.toImage()); +} + +void ChatPage::syncCompleted(SyncResponse response) +{ + matrix_client_->setNextBatchToken(response.nextBatch()); + + /* room_list_->sync(response.rooms()); */ + view_manager_->sync(response.rooms()); +} + +void ChatPage::initialSyncCompleted(SyncResponse response) +{ + if (!response.nextBatch().isEmpty()) + matrix_client_->setNextBatchToken(response.nextBatch()); + + view_manager_->initialize(response.rooms()); + room_list_->setInitialRooms(response.rooms()); + + sync_timer_ = new QTimer(this); + connect(sync_timer_, SIGNAL(timeout()), this, SLOT(startSync())); + sync_timer_->start(sync_interval_); +} + +// TODO: This function should be part of the matrix client for generic media retrieval. +void ChatPage::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url) +{ + // TODO: move this into a Utils function + QList url_parts = avatar_url.toString().split("mxc://"); + + if (url_parts.size() != 2) { + qDebug() << "Invalid format for room avatar " << avatar_url.toString(); + return; + } + + QString media_params = url_parts[1]; + QString media_url = QString("%1/_matrix/media/r0/download/%2") + .arg(matrix_client_->getHomeServer(), media_params); + + QNetworkRequest avatar_request(media_url); + QNetworkReply *reply = content_downloader_->get(avatar_request); + reply->setProperty("media_params", media_params); + + connect(reply, &QNetworkReply::finished, [this, media_params, roomid, reply]() { + reply->deleteLater(); + + auto media = reply->property("media_params").toString(); + + if (media != media_params) + return; + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status == 0) { + qDebug() << reply->errorString(); + return; + } + + if (status >= 400) { + qWarning() << "Request " << reply->request().url() << " returned " << status; + return; + } + + auto img = reply->readAll(); + + if (img.size() == 0) + return; + + QPixmap pixmap; + pixmap.loadFromData(img); + room_avatars_.insert(roomid, pixmap); + + this->room_list_->updateRoomAvatar(roomid, pixmap.toImage()); + + if (current_room_.id() == roomid) { + QIcon icon(pixmap); + this->top_bar_->updateRoomAvatar(icon); + } + }); +} + +void ChatPage::updateOwnProfileInfo(QUrl avatar_url, QString display_name) +{ + QSettings settings; + auto userid = settings.value("auth/user_id").toString(); + + user_info_widget_->setUserId(userid); + user_info_widget_->setDisplayName(display_name); + + // TODO: move this into a Utils function + QList url_parts = avatar_url.toString().split("mxc://"); + + if (url_parts.size() != 2) { + qDebug() << "Invalid format for media " << avatar_url.toString(); + return; + } + + QString media_params = url_parts[1]; + QString media_url = QString("%1/_matrix/media/r0/download/%2") + .arg(matrix_client_->getHomeServer(), media_params); + + QNetworkRequest avatar_request(media_url); + QNetworkReply *reply = content_downloader_->get(avatar_request); + reply->setProperty("media_params", media_params); + + connect(reply, &QNetworkReply::finished, [this, media_params, reply]() { + reply->deleteLater(); + + auto media = reply->property("media_params").toString(); + + if (media != media_params) + return; + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status == 0) { + qDebug() << reply->errorString(); + return; + } + + if (status >= 400) { + qWarning() << "Request " << reply->request().url() << " returned " << status; + return; + } + + setOwnAvatar(reply->readAll()); + }); +} + +void ChatPage::changeTopRoomInfo(const RoomInfo &info) +{ + top_bar_->updateRoomName(info.name()); + top_bar_->updateRoomTopic(info.topic()); + + if (room_avatars_.contains(info.id())) { + QIcon icon(room_avatars_.value(info.id())); + top_bar_->updateRoomAvatar(icon); + } + + current_room_ = info; +} + +ChatPage::~ChatPage() +{ + sync_timer_->stop(); + delete ui; +} diff --git a/src/Deserializable.cc b/src/Deserializable.cc new file mode 100644 index 00000000..967f5ef4 --- /dev/null +++ b/src/Deserializable.cc @@ -0,0 +1,31 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "Deserializable.h" + +DeserializationException::DeserializationException(const std::string &msg) : msg_(msg) +{ +} + +const char *DeserializationException::what() const throw() +{ + return msg_.c_str(); +} diff --git a/src/HistoryView.cc b/src/HistoryView.cc new file mode 100644 index 00000000..0949d17c --- /dev/null +++ b/src/HistoryView.cc @@ -0,0 +1,145 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "HistoryView.h" +#include "HistoryViewItem.h" + +const QList HistoryView::COLORS({"#FFF46E", + "#A58BFF", + "#50C9BA", + "#9EE6CF", + "#FFDD67", + "#2980B9", + "#FC993C", + "#2772DB", + "#CB8589", + "#DDE8B9", + "#55A44E", + "#A9EEE6", + "#53B759", + "#9E3997", + "#5D89D5", + "#BB86B7", + "#50a0cf", + "#3C989F", + "#5A4592", + "#235e5b", + "#d58247", + "#e0a729", + "#a2b636", + "#4BBE2E"}); + +HistoryView::HistoryView(QList events, QWidget *parent) + : QWidget(parent) +{ + init(); + addEvents(events); +} + +HistoryView::HistoryView(QWidget *parent) + : QWidget(parent) +{ + init(); +} + +void HistoryView::sliderRangeChanged(int min, int max) +{ + Q_UNUSED(min); + scroll_area_->verticalScrollBar()->setValue(max); +} + +QString HistoryView::chooseRandomColor() +{ + std::random_device random_device; + std::mt19937 engine{random_device()}; + std::uniform_int_distribution dist(0, HistoryView::COLORS.size() - 1); + + return HistoryView::COLORS[dist(engine)]; +} + +void HistoryView::addEvents(const QList &events) +{ + for (int i = 0; i < events.size(); i++) { + auto event = events[i]; + + if (event.type() == "m.room.message") { + auto msg_type = event.content().value("msgtype").toString(); + + if (msg_type == "m.text" || msg_type == "m.notice") { + auto with_sender = last_sender_ != event.sender(); + auto color = nick_colors_.value(event.sender()); + + if (color.isEmpty()) { + color = chooseRandomColor(); + nick_colors_.insert(event.sender(), color); + } + + addHistoryItem(event, color, with_sender); + last_sender_ = event.sender(); + } else { + qDebug() << "Ignoring message" << msg_type; + } + } + } +} + +void HistoryView::init() +{ + top_layout_ = new QVBoxLayout(this); + top_layout_->setSpacing(0); + top_layout_->setMargin(0); + + scroll_area_ = new QScrollArea(this); + scroll_area_->setWidgetResizable(true); + + scroll_widget_ = new QWidget(); + + scroll_layout_ = new QVBoxLayout(); + scroll_layout_->addStretch(1); + scroll_layout_->setSpacing(0); + + scroll_widget_->setLayout(scroll_layout_); + + scroll_area_->setWidget(scroll_widget_); + + top_layout_->addWidget(scroll_area_); + + setLayout(top_layout_); + + connect(scroll_area_->verticalScrollBar(), + SIGNAL(rangeChanged(int, int)), + this, + SLOT(sliderRangeChanged(int, int))); +} + +void HistoryView::addHistoryItem(Event event, QString color, bool with_sender) +{ + // TODO: Probably create another function instead of passing the flag. + HistoryViewItem *item = new HistoryViewItem(event, with_sender, color, scroll_widget_); + scroll_layout_->addWidget(item); +} + +HistoryView::~HistoryView() +{ +} diff --git a/src/HistoryViewItem.cc b/src/HistoryViewItem.cc new file mode 100644 index 00000000..04c42f45 --- /dev/null +++ b/src/HistoryViewItem.cc @@ -0,0 +1,80 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "HistoryViewItem.h" + +HistoryViewItem::HistoryViewItem(Event event, bool with_sender, QString color, QWidget *parent) + : QWidget(parent) +{ + QString sender = ""; + + if (with_sender) + sender = event.sender().split(":")[0].split("@")[1]; + + auto body = event.content().value("body").toString(); + + auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); + auto local_time = timestamp.toString("HH:mm"); + + time_label_ = new QLabel(this); + time_label_->setWordWrap(true); + QString msg( + "" + "" + "" + " " + " %1" + " " + "" + ""); + time_label_->setText(msg.arg(local_time)); + time_label_->setStyleSheet("margin-left: 7px; margin-right: 7px; margin-top: 0;"); + time_label_->setAlignment(Qt::AlignTop); + + content_label_ = new QLabel(this); + content_label_->setWordWrap(true); + content_label_->setAlignment(Qt::AlignTop); + content_label_->setStyleSheet("margin: 0;"); + QString content( + "" + "" + "" + " " + " %2" + " " + " " + " %3" + " " + "" + ""); + content_label_->setText(content.arg(color).arg(sender).arg(body)); + + top_layout_ = new QHBoxLayout(); + top_layout_->setMargin(0); + + top_layout_->addWidget(time_label_); + top_layout_->addWidget(content_label_, 1); + + setLayout(top_layout_); +} + +HistoryViewItem::~HistoryViewItem() +{ +} diff --git a/src/HistoryViewManager.cc b/src/HistoryViewManager.cc new file mode 100644 index 00000000..c7292747 --- /dev/null +++ b/src/HistoryViewManager.cc @@ -0,0 +1,83 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "HistoryView.h" +#include "HistoryViewManager.h" + +HistoryViewManager::HistoryViewManager(QWidget *parent) + : QStackedWidget(parent) +{ + setStyleSheet( + "QWidget {background: #171919; color: #ebebeb;}" + "QScrollBar:vertical { background-color: #171919; width: 10px; border-radius: 20px; margin: 0px 2px 0 2px; }" + "QScrollBar::handle:vertical { border-radius : 50px; background-color : #1c3133; }" + "QScrollBar::add-line:vertical { border: none; background: none; }" + "QScrollBar::sub-line:vertical { border: none; background: none; }"); +} + +HistoryViewManager::~HistoryViewManager() +{ +} + +void HistoryViewManager::initialize(const Rooms &rooms) +{ + for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) { + auto roomid = it.key(); + auto events = it.value().timeline().events(); + + // Create a history view with the room events. + HistoryView *view = new HistoryView(events); + views_.insert(it.key(), view); + + // Add the view in the widget stack. + addWidget(view); + } +} + +void HistoryViewManager::sync(const Rooms &rooms) +{ + for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) { + auto roomid = it.key(); + + if (!views_.contains(roomid)) { + qDebug() << "Ignoring event from unknown room"; + continue; + } + + auto view = views_.value(roomid); + auto events = it.value().timeline().events(); + + view->addEvents(events); + } +} + +void HistoryViewManager::setHistoryView(const RoomInfo &info) +{ + if (!views_.contains(info.id())) { + qDebug() << "Room List id is not present in view manager"; + qDebug() << info.name(); + return; + } + + auto widget = views_.value(info.id()); + + setCurrentWidget(widget); +} diff --git a/src/InputValidator.cc b/src/InputValidator.cc new file mode 100644 index 00000000..3713c501 --- /dev/null +++ b/src/InputValidator.cc @@ -0,0 +1,31 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "InputValidator.h" + +// FIXME: Maybe change the regex to match the real Matrix ID format and not email. +InputValidator::InputValidator(QObject *parent) + : matrix_id_("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}") + , matrix_localpart_("[A-za-z0-9._%+-]{3,}") + , matrix_password_(".{8,}") + , server_domain_("(?!\\-)(?:[a-zA-Z\\d\\-]{0,62}[a-zA-Z\\d]\\.){1,126}(?!\\d+)[a-zA-Z\\d]{1,63}") +{ + id_ = new QRegExpValidator(matrix_id_, parent); + localpart_ = new QRegExpValidator(matrix_localpart_, parent); + password_ = new QRegExpValidator(matrix_password_, parent); + domain_ = new QRegExpValidator(server_domain_, parent); +} diff --git a/src/Login.cc b/src/Login.cc new file mode 100644 index 00000000..f3b8e2f4 --- /dev/null +++ b/src/Login.cc @@ -0,0 +1,89 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "Deserializable.h" +#include "Login.h" + +LoginRequest::LoginRequest() +{ +} + +LoginRequest::LoginRequest(QString username, QString password) + : user_(username) + , password_(password) +{ +} + +QByteArray LoginRequest::serialize() +{ + QJsonObject body{ + {"type", "m.login.password"}, + {"user", user_}, + {"password", password_}}; + + return QJsonDocument(body).toJson(QJsonDocument::Compact); +} + +void LoginRequest::setPassword(QString password) +{ + password_ = password; +} + +void LoginRequest::setUser(QString username) +{ + user_ = username; +} + +QString LoginResponse::getAccessToken() +{ + return access_token_; +} + +QString LoginResponse::getHomeServer() +{ + return home_server_; +} + +QString LoginResponse::getUserId() +{ + return user_id_; +} + +void LoginResponse::deserialize(QJsonDocument data) throw(DeserializationException) +{ + if (!data.isObject()) + throw DeserializationException("Login response is not a JSON object"); + + QJsonObject object = data.object(); + + if (object.value("access_token") == QJsonValue::Undefined) + throw DeserializationException("Login: missing access_token param"); + + if (object.value("home_server") == QJsonValue::Undefined) + throw DeserializationException("Login: missing home_server param"); + + if (object.value("user_id") == QJsonValue::Undefined) + throw DeserializationException("Login: missing user_id param"); + + access_token_ = object.value("access_token").toString(); + home_server_ = object.value("home_server").toString(); + user_id_ = object.value("user_id").toString(); +} diff --git a/src/LoginPage.cc b/src/LoginPage.cc new file mode 100644 index 00000000..68927c33 --- /dev/null +++ b/src/LoginPage.cc @@ -0,0 +1,147 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "LoginPage.h" + +LoginPage::LoginPage(QWidget *parent) + : QWidget(parent) + , matrix_id_validator_(new InputValidator(parent)) +{ + top_layout_ = new QVBoxLayout(); + + back_layout_ = new QHBoxLayout(); + back_layout_->setSpacing(0); + back_layout_->setContentsMargins(5, 5, -1, -1); + + back_button_ = new FlatButton(this); + back_button_->setMinimumSize(QSize(30, 30)); + back_button_->setCursor(QCursor(Qt::PointingHandCursor)); + + QIcon icon; + icon.addFile(":/icons/icons/left-angle.png", QSize(), QIcon::Normal, QIcon::Off); + + back_button_->setIcon(icon); + back_button_->setIconSize(QSize(24, 24)); + + back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); + back_layout_->addStretch(1); + + logo_layout_ = new QHBoxLayout(); + logo_layout_->setContentsMargins(0, 20, 0, 20); + logo_ = new QLabel(this); + logo_->setText("nheko"); + logo_->setStyleSheet("font-size: 22pt; font-weight: 400;"); + logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); + + form_wrapper_ = new QHBoxLayout(); + form_widget_ = new QWidget(); + form_widget_->setMinimumSize(QSize(350, 200)); + + form_layout_ = new QVBoxLayout(); + form_layout_->setSpacing(20); + form_layout_->setContentsMargins(0, 00, 0, 30); + form_widget_->setLayout(form_layout_); + + form_wrapper_->addStretch(1); + form_wrapper_->addWidget(form_widget_); + form_wrapper_->addStretch(1); + + username_input_ = new TextField(); + username_input_->setLabel("Username"); + username_input_->setInkColor("#577275"); + username_input_->setBackgroundColor("#f9f9f9"); + + password_input_ = new TextField(); + password_input_->setLabel("Password"); + password_input_->setInkColor("#577275"); + password_input_->setBackgroundColor("#f9f9f9"); + password_input_->setEchoMode(QLineEdit::Password); + + form_layout_->addWidget(username_input_, Qt::AlignHCenter, 0); + form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0); + + button_layout_ = new QHBoxLayout(); + button_layout_->setSpacing(0); + button_layout_->setContentsMargins(0, 0, 0, 50); + + login_button_ = new RaisedButton("LOGIN", this); + login_button_->setBackgroundColor(QColor("#171919")); + login_button_->setForegroundColor(QColor("#ebebeb")); + login_button_->setMinimumSize(350, 65); + login_button_->setCursor(QCursor(Qt::PointingHandCursor)); + login_button_->setFontSize(17); + login_button_->setCornerRadius(3); + + button_layout_->addStretch(1); + button_layout_->addWidget(login_button_); + button_layout_->addStretch(1); + + error_label_ = new QLabel(this); + error_label_->setStyleSheet("color: #E22826; font-size: 11pt;"); + + top_layout_->addLayout(back_layout_); + top_layout_->addStretch(1); + top_layout_->addLayout(logo_layout_); + top_layout_->addLayout(form_wrapper_); + top_layout_->addStretch(1); + top_layout_->addLayout(button_layout_); + top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); + top_layout_->addStretch(1); + + connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); + connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked())); + connect(username_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + + username_input_->setValidator(matrix_id_validator_->id_); + + setLayout(top_layout_); +} + +void LoginPage::loginError(QString error) +{ + qWarning() << "Error Message: " << error; + error_label_->setText(error); +} + +void LoginPage::onLoginButtonClicked() +{ + error_label_->setText(""); + + if (!username_input_->hasAcceptableInput()) { + loginError("Invalid Matrix ID"); + } else if (password_input_->text().isEmpty()) { + loginError("Empty password"); + } else { + QString user = username_input_->text().split("@").at(0); + QString home_server = username_input_->text().split("@").at(1); + QString password = password_input_->text(); + + emit userLogin(user, password, home_server); + } +} + +void LoginPage::onBackButtonClicked() +{ + emit backButtonClicked(); +} + +LoginPage::~LoginPage() +{ +} diff --git a/src/MainWindow.cc b/src/MainWindow.cc new file mode 100644 index 00000000..82976f23 --- /dev/null +++ b/src/MainWindow.cc @@ -0,0 +1,134 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "MainWindow.h" +#include "ui_MainWindow.h" + +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui_(new Ui::MainWindow) + , welcome_page_(new WelcomePage(parent)) + , login_page_(new LoginPage(parent)) + , register_page_(new RegisterPage(parent)) + , chat_page_(new ChatPage(parent)) + , matrix_client_(new MatrixClient("matrix.org", parent)) +{ + ui_->setupUi(this); + + // Initialize sliding widget manager. + sliding_stack_ = new SlidingStackWidget(this); + sliding_stack_->addWidget(welcome_page_); + sliding_stack_->addWidget(login_page_); + sliding_stack_->addWidget(register_page_); + sliding_stack_->addWidget(chat_page_); + + setCentralWidget(sliding_stack_); + + connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage())); + connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage())); + + connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); + connect(login_page_, + SIGNAL(userLogin(const QString &, const QString &, const QString &)), + this, + SLOT(matrixLogin(const QString &, const QString &, const QString &))); + + connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); + connect(register_page_, + SIGNAL(registerUser(const QString &, const QString &, const QString &)), + this, + SLOT(matrixRegister(const QString &, const QString &, const QString &))); + + connect(matrix_client_, SIGNAL(loginError(QString)), login_page_, SLOT(loginError(QString))); + connect(matrix_client_, + SIGNAL(loginSuccess(QString, QString, QString)), + this, + SLOT(showChatPage(QString, QString, QString))); +} + +void MainWindow::matrixLogin(const QString &username, const QString &password, const QString &home_server) +{ + qDebug() << "About to login into Matrix"; + qDebug() << "Userame: " << username; + + matrix_client_->setServer(home_server); + matrix_client_->login(username, password); +} + +void MainWindow::showChatPage(QString userid, QString homeserver, QString token) +{ + QSettings settings; + settings.setValue("auth/access_token", token); + settings.setValue("auth/home_server", homeserver); + settings.setValue("auth/user_id", userid); + + int index = sliding_stack_->getWidgetIndex(chat_page_); + sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::LEFT_TO_RIGHT); + + chat_page_->bootstrap(userid, homeserver, token); +} + +void MainWindow::matrixRegister(const QString &username, const QString &password, const QString &server) +{ + qDebug() << "About to register to Matrix"; + qDebug() << "Username: " << username << " Password: " << password << " Server: " << server; +} + +void MainWindow::showWelcomePage() +{ + int index = sliding_stack_->getWidgetIndex(welcome_page_); + + if (sliding_stack_->currentIndex() == sliding_stack_->getWidgetIndex(login_page_)) + sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::RIGHT_TO_LEFT); + else + sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::LEFT_TO_RIGHT); +} + +void MainWindow::showLoginPage() +{ + QSettings settings; + + if (settings.contains("auth/access_token") && + settings.contains("auth/home_server") && + settings.contains("auth/user_id")) { + QString token = settings.value("auth/access_token").toString(); + QString home_server = settings.value("auth/home_server").toString(); + QString user_id = settings.value("auth/user_id").toString(); + + showChatPage(user_id, home_server, token); + + return; + } + + int index = sliding_stack_->getWidgetIndex(login_page_); + sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::LEFT_TO_RIGHT); +} + +void MainWindow::showRegisterPage() +{ + int index = sliding_stack_->getWidgetIndex(register_page_); + sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::RIGHT_TO_LEFT); +} + +MainWindow::~MainWindow() +{ + delete ui_; +} diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc new file mode 100644 index 00000000..5510b6d9 --- /dev/null +++ b/src/MatrixClient.cc @@ -0,0 +1,365 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 +#include + +#include "Login.h" +#include "MatrixClient.h" +#include "Profile.h" + +MatrixClient::MatrixClient(QString server, QObject *parent) + : QNetworkAccessManager(parent) +{ + server_ = "https://" + server; + api_url_ = "/_matrix/client/r0"; + token_ = ""; + + QSettings settings; + txn_id_ = settings.value("client/transaction_id", 1).toInt(); + + // FIXME: Other QNetworkAccessManagers use the finish handler. + connect(this, SIGNAL(finished(QNetworkReply *)), this, SLOT(onResponse(QNetworkReply *))); +} + +MatrixClient::~MatrixClient() +{ +} + +void MatrixClient::onVersionsResponse(QNetworkReply *reply) +{ + reply->deleteLater(); + + qDebug() << "Handling the versions response"; + + auto data = reply->readAll(); + auto json = QJsonDocument::fromJson(data); + + qDebug() << json; +} + +void MatrixClient::onLoginResponse(QNetworkReply *reply) +{ + reply->deleteLater(); + + int status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status_code == 403) { + emit loginError("Wrong username or password"); + return; + } + + if (status_code == 404) { + emit loginError("Login endpoint was not found on the server"); + return; + } + + if (status_code != 200) { + qDebug() << "Login response: status code " << status_code; + + if (status_code >= 400) { + qWarning() << "Login error: " << reply->errorString(); + emit loginError("An unknown error occured. Please try again."); + return; + } + } + + auto data = reply->readAll(); + auto json = QJsonDocument::fromJson(data); + + LoginResponse response; + + try { + response.deserialize(json); + emit loginSuccess(response.getUserId(), + response.getHomeServer(), + response.getAccessToken()); + } catch (DeserializationException &e) { + qWarning() << "Malformed JSON response" << e.what(); + emit loginError("Malformed response. Possibly not a Matrix server"); + } +} + +void MatrixClient::onRegisterResponse(QNetworkReply *reply) +{ + reply->deleteLater(); + + qDebug() << "Handling the register response"; +} + +void MatrixClient::onGetOwnProfileResponse(QNetworkReply *reply) +{ + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status >= 400) { + qWarning() << reply->errorString(); + return; + } + + auto data = reply->readAll(); + auto json = QJsonDocument::fromJson(data); + + ProfileResponse response; + + try { + response.deserialize(json); + emit getOwnProfileResponse(response.getAvatarUrl(), response.getDisplayName()); + } catch (DeserializationException &e) { + qWarning() << "Profile malformed response" << e.what(); + } +} + +void MatrixClient::onInitialSyncResponse(QNetworkReply *reply) +{ + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status == 0 || status >= 400) { + qWarning() << reply->errorString(); + return; + } + + auto data = reply->readAll(); + + if (data.isEmpty()) + return; + + auto json = QJsonDocument::fromJson(data); + + SyncResponse response; + + try { + response.deserialize(json); + emit initialSyncCompleted(response); + } catch (DeserializationException &e) { + qWarning() << "Sync malformed response" << e.what(); + } +} + +void MatrixClient::onSyncResponse(QNetworkReply *reply) +{ + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status == 0 || status >= 400) { + qWarning() << reply->errorString(); + return; + } + + auto data = reply->readAll(); + + if (data.isEmpty()) + return; + + auto json = QJsonDocument::fromJson(data); + + SyncResponse response; + + try { + response.deserialize(json); + emit syncCompleted(response); + } catch (DeserializationException &e) { + qWarning() << "Sync malformed response" << e.what(); + } +} + +void MatrixClient::onSendTextMessageResponse(QNetworkReply *reply) +{ + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status == 0 || status >= 400) { + qWarning() << reply->errorString(); + return; + } + + auto data = reply->readAll(); + + if (data.isEmpty()) + return; + + auto json = QJsonDocument::fromJson(data); + + if (!json.isObject()) { + qDebug() << "Send message response is not a JSON object"; + return; + } + + auto object = json.object(); + + if (!object.contains("event_id")) { + qDebug() << "SendTextMessage: missnig event_id from response"; + return; + } + + emit messageSent(object.value("event_id").toString(), + reply->property("txn_id").toInt()); + + incrementTransactionId(); +} + +void MatrixClient::onResponse(QNetworkReply *reply) +{ + switch (reply->property("endpoint").toInt()) { + case Endpoint::Versions: + onVersionsResponse(reply); + break; + case Endpoint::Login: + onLoginResponse(reply); + break; + case Endpoint::Register: + onRegisterResponse(reply); + case Endpoint::GetOwnProfile: + onGetOwnProfileResponse(reply); + case Endpoint::InitialSync: + onInitialSyncResponse(reply); + case Endpoint::Sync: + onSyncResponse(reply); + case Endpoint::SendTextMessage: + onSendTextMessageResponse(reply); + default: + break; + } +} + +void MatrixClient::login(const QString &username, const QString &password) +{ + QUrl endpoint(server_); + endpoint.setPath(api_url_ + "/login"); + + QNetworkRequest request(endpoint); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + LoginRequest body(username, password); + + QNetworkReply *reply = post(request, body.serialize()); + reply->setProperty("endpoint", Endpoint::Login); +} + +void MatrixClient::sync() +{ + QJsonObject filter{{"room", + QJsonObject{{"ephemeral", QJsonObject{{"limit", 0}}}}}, + {"presence", QJsonObject{{"limit", 0}}}}; + + QUrlQuery query; + query.addQueryItem("set_presence", "online"); + query.addQueryItem("filter", QJsonDocument(filter).toJson(QJsonDocument::Compact)); + query.addQueryItem("access_token", token_); + + if (next_batch_.isEmpty()) { + qDebug() << "Sync requires a valid next_batch token. Initial sync should be performed."; + return; + } + + query.addQueryItem("since", next_batch_); + + QUrl endpoint(server_); + endpoint.setPath(api_url_ + "/sync"); + endpoint.setQuery(query); + + QNetworkRequest request(QString(endpoint.toEncoded())); + + QNetworkReply *reply = get(request); + reply->setProperty("endpoint", Endpoint::Sync); +} + +void MatrixClient::sendTextMessage(QString roomid, QString msg) +{ + QUrlQuery query; + query.addQueryItem("access_token", token_); + + QUrl endpoint(server_); + endpoint.setPath(api_url_ + QString("/rooms/%1/send/m.room.message/%2").arg(roomid).arg(txn_id_)); + endpoint.setQuery(query); + + QJsonObject body{ + {"msgtype", "m.text"}, + {"body", msg}}; + + QNetworkRequest request(QString(endpoint.toEncoded())); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QNetworkReply *reply = put(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); + + reply->setProperty("endpoint", Endpoint::SendTextMessage); + reply->setProperty("txn_id", txn_id_); +} + +void MatrixClient::initialSync() +{ + QJsonObject filter{{"room", + QJsonObject{{"timeline", QJsonObject{{"limit", 70}}}, + {"ephemeral", QJsonObject{{"limit", 0}}}}}, + {"presence", QJsonObject{{"limit", 0}}}}; + + QUrlQuery query; + query.addQueryItem("full_state", "true"); + query.addQueryItem("set_presence", "online"); + query.addQueryItem("filter", QJsonDocument(filter).toJson(QJsonDocument::Compact)); + query.addQueryItem("access_token", token_); + + QUrl endpoint(server_); + endpoint.setPath(api_url_ + "/sync"); + endpoint.setQuery(query); + + QNetworkRequest request(QString(endpoint.toEncoded())); + + QNetworkReply *reply = get(request); + reply->setProperty("endpoint", Endpoint::InitialSync); +} + +void MatrixClient::versions() +{ + QUrl endpoint(server_); + endpoint.setPath("/_matrix/client/versions"); + + QNetworkRequest request(endpoint); + + QNetworkReply *reply = get(request); + reply->setProperty("endpoint", Endpoint::Versions); +} + +void MatrixClient::getOwnProfile() +{ + // FIXME: Remove settings from the matrix client. The class should store the user's matrix ID. + QSettings settings; + auto userid = settings.value("auth/user_id", "").toString(); + + QUrlQuery query; + query.addQueryItem("access_token", token_); + + QUrl endpoint(server_); + endpoint.setPath(api_url_ + "/profile/" + userid); + endpoint.setQuery(query); + + QNetworkRequest request(QString(endpoint.toEncoded())); + + QNetworkReply *reply = get(request); + reply->setProperty("endpoint", Endpoint::GetOwnProfile); +} diff --git a/src/Profile.cc b/src/Profile.cc new file mode 100644 index 00000000..aa556370 --- /dev/null +++ b/src/Profile.cc @@ -0,0 +1,50 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "Deserializable.h" +#include "Profile.h" + +QUrl ProfileResponse::getAvatarUrl() +{ + return avatar_url_; +} + +QString ProfileResponse::getDisplayName() +{ + return display_name_; +} + +void ProfileResponse::deserialize(QJsonDocument data) throw(DeserializationException) +{ + if (!data.isObject()) + throw DeserializationException("Profile response is not a JSON object"); + + QJsonObject object = data.object(); + + if (object.value("avatar_url") == QJsonValue::Undefined) + throw DeserializationException("Profile: missing avatar_url param"); + + if (object.value("displayname") == QJsonValue::Undefined) + throw DeserializationException("Profile: missing displayname param"); + + avatar_url_ = QUrl(object.value("avatar_url").toString()); + display_name_ = object.value("displayname").toString(); +} diff --git a/src/RegisterPage.cc b/src/RegisterPage.cc new file mode 100644 index 00000000..fcb43b86 --- /dev/null +++ b/src/RegisterPage.cc @@ -0,0 +1,166 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "RegisterPage.h" + +RegisterPage::RegisterPage(QWidget *parent) + : QWidget(parent) + , validator_(new InputValidator(parent)) +{ + top_layout_ = new QVBoxLayout(); + + back_layout_ = new QHBoxLayout(); + back_layout_->setSpacing(0); + back_layout_->setContentsMargins(5, 5, -1, -1); + + back_button_ = new FlatButton(this); + back_button_->setMinimumSize(QSize(30, 30)); + back_button_->setCursor(QCursor(Qt::PointingHandCursor)); + + QIcon icon; + icon.addFile(":/icons/icons/left-angle.png", QSize(), QIcon::Normal, QIcon::Off); + + back_button_->setIcon(icon); + back_button_->setIconSize(QSize(24, 24)); + + back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); + back_layout_->addStretch(1); + + logo_layout_ = new QHBoxLayout(); + logo_layout_->setContentsMargins(0, 20, 0, 20); + logo_ = new QLabel(this); + logo_->setText("nheko"); + logo_->setStyleSheet("font-size: 22pt; font-weight: 400;"); + logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); + + form_wrapper_ = new QHBoxLayout(); + form_widget_ = new QWidget(); + form_widget_->setMinimumSize(QSize(350, 300)); + + form_layout_ = new QVBoxLayout(); + form_layout_->setSpacing(20); + form_layout_->setContentsMargins(0, 00, 0, 60); + form_widget_->setLayout(form_layout_); + + form_wrapper_->addStretch(1); + form_wrapper_->addWidget(form_widget_); + form_wrapper_->addStretch(1); + + username_input_ = new TextField(); + username_input_->setLabel("Username"); + username_input_->setInkColor("#577275"); + username_input_->setBackgroundColor("#f9f9f9"); + + password_input_ = new TextField(); + password_input_->setLabel("Password"); + password_input_->setInkColor("#577275"); + password_input_->setBackgroundColor("#f9f9f9"); + password_input_->setEchoMode(QLineEdit::Password); + + password_confirmation_ = new TextField(); + password_confirmation_->setLabel("Password confirmation"); + password_confirmation_->setInkColor("#577275"); + password_confirmation_->setBackgroundColor("#f9f9f9"); + password_confirmation_->setEchoMode(QLineEdit::Password); + + server_input_ = new TextField(); + server_input_->setLabel("Home Server"); + server_input_->setInkColor("#577275"); + server_input_->setBackgroundColor("#f9f9f9"); + + form_layout_->addWidget(username_input_, Qt::AlignHCenter, 0); + form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0); + form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter, 0); + form_layout_->addWidget(server_input_, Qt::AlignHCenter, 0); + + button_layout_ = new QHBoxLayout(); + button_layout_->setSpacing(0); + button_layout_->setContentsMargins(0, 0, 0, 50); + + register_button_ = new RaisedButton("REGISTER", this); + register_button_->setBackgroundColor(QColor("#171919")); + register_button_->setForegroundColor(QColor("#ebebeb")); + register_button_->setMinimumSize(350, 65); + register_button_->setCursor(QCursor(Qt::PointingHandCursor)); + register_button_->setFontSize(17); + register_button_->setCornerRadius(3); + + button_layout_->addStretch(1); + button_layout_->addWidget(register_button_); + button_layout_->addStretch(1); + + top_layout_->addLayout(back_layout_); + top_layout_->addStretch(1); + top_layout_->addLayout(logo_layout_); + top_layout_->addLayout(form_wrapper_); + top_layout_->addStretch(2); + top_layout_->addLayout(button_layout_); + top_layout_->addStretch(1); + + connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); + connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked())); + + connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + + username_input_->setValidator(validator_->localpart_); + password_input_->setValidator(validator_->password_); + server_input_->setValidator(validator_->domain_); + + setLayout(top_layout_); +} + +void RegisterPage::onBackButtonClicked() +{ + emit backButtonClicked(); +} + +void RegisterPage::onRegisterButtonClicked() +{ + if (!username_input_->hasAcceptableInput()) { + QString text("Invalid username"); + QPoint point = username_input_->mapToGlobal(username_input_->rect().topRight()); + QToolTip::showText(point, text); + } else if (!password_input_->hasAcceptableInput()) { + QString text("Password is not long enough"); + QPoint point = password_input_->mapToGlobal(password_input_->rect().topRight()); + QToolTip::showText(point, text); + } else if (password_input_->text() != password_confirmation_->text()) { + QString text("Passwords don't match"); + QPoint point = password_confirmation_->mapToGlobal(password_confirmation_->rect().topRight()); + QToolTip::showText(point, text); + } else if (!server_input_->hasAcceptableInput()) { + QString text("Invalid server name"); + QPoint point = server_input_->mapToGlobal(server_input_->rect().topRight()); + QToolTip::showText(point, text); + } else { + QString username = username_input_->text(); + QString password = password_input_->text(); + QString server = server_input_->text(); + + emit registerUser(username, password, server); + } +} + +RegisterPage::~RegisterPage() +{ +} diff --git a/src/RoomInfo.cc b/src/RoomInfo.cc new file mode 100644 index 00000000..f8a7c56a --- /dev/null +++ b/src/RoomInfo.cc @@ -0,0 +1,71 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "RoomInfo.h" + +RoomInfo::RoomInfo() + : name_("") + , topic_("") +{ +} + +RoomInfo::RoomInfo(QString name, QString topic, QUrl avatar_url) + : name_(name) + , topic_(topic) + , avatar_url_(avatar_url) +{ +} + +QString RoomInfo::id() const +{ + return id_; +} + +QString RoomInfo::name() const +{ + return name_; +} + +QString RoomInfo::topic() const +{ + return topic_; +} + +QUrl RoomInfo::avatarUrl() const +{ + return avatar_url_; +} + +void RoomInfo::setAvatarUrl(const QUrl &url) +{ + avatar_url_ = url; +} + +void RoomInfo::setId(const QString &id) +{ + id_ = id; +} + +void RoomInfo::setName(const QString &name) +{ + name_ = name; +} + +void RoomInfo::setTopic(const QString &topic) +{ + topic_ = topic; +} diff --git a/src/RoomInfoListItem.cc b/src/RoomInfoListItem.cc new file mode 100644 index 00000000..dedae3fd --- /dev/null +++ b/src/RoomInfoListItem.cc @@ -0,0 +1,138 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "Ripple.h" +#include "RoomInfo.h" +#include "RoomInfoListItem.h" + +RoomInfoListItem::RoomInfoListItem(RoomInfo info, QWidget *parent) + : QWidget(parent) + , info_(info) + , is_pressed_(false) + , max_height_(65) +{ + normal_style_ = + "QWidget { background-color: #5d6565; color: #ebebeb;" + "border-bottom: 1px solid #171919;}" + "QLabel { border: none; }"; + + pressed_style_ = + "QWidget { background-color: #577275; color: #ebebeb;" + "border-bottom: 1px solid #171919;}" + "QLabel { border: none; }"; + + setStyleSheet(normal_style_); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setAutoFillBackground(true); + + setMinimumSize(parent->width(), max_height_); + + topLayout_ = new QHBoxLayout(this); + topLayout_->setSpacing(0); + topLayout_->setMargin(0); + + avatarWidget_ = new QWidget(this); + avatarWidget_->setMaximumSize(max_height_, max_height_); + textWidget_ = new QWidget(this); + + avatarLayout_ = new QVBoxLayout(avatarWidget_); + avatarLayout_->setSpacing(0); + avatarLayout_->setContentsMargins(0, 5, 0, 5); + + textLayout_ = new QVBoxLayout(textWidget_); + textLayout_->setSpacing(0); + textLayout_->setContentsMargins(0, 5, 0, 5); + + roomAvatar_ = new Avatar(avatarWidget_); + roomAvatar_->setLetter(QChar(info_.name()[0])); + avatarLayout_->addWidget(roomAvatar_); + + roomName_ = new QLabel(textWidget_); + roomName_->setText(info_.name()); + roomName_->setMaximumSize(230, max_height_ / 2); + roomName_->setStyleSheet("font-weight: 500; font-size: 11.5pt"); + roomName_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + + roomTopic_ = new QLabel(textWidget_); + roomTopic_->setText(info_.topic()); + roomTopic_->setMaximumSize(230, max_height_ / 2); + roomTopic_->setStyleSheet("color: #c9c9c9; font-size: 10pt"); + roomTopic_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + + textLayout_->addWidget(roomName_); + textLayout_->addWidget(roomTopic_); + + topLayout_->addWidget(avatarWidget_); + topLayout_->addWidget(textWidget_); + + setElidedText(roomName_, info_.name(), 220); + setElidedText(roomTopic_, info_.topic(), 220); + + QPainterPath path; + path.addRoundedRect(rect(), 0, 0); + + ripple_overlay_ = new RippleOverlay(this); + ripple_overlay_->setClipPath(path); + ripple_overlay_->setClipping(true); + + setLayout(topLayout_); +} + +void RoomInfoListItem::setPressedState(bool state) +{ + if (!is_pressed_ && state) { + is_pressed_ = state; + setStyleSheet(pressed_style_); + } else if (is_pressed_ && !state) { + is_pressed_ = state; + setStyleSheet(normal_style_); + } +} + +void RoomInfoListItem::mousePressEvent(QMouseEvent *event) +{ + emit clicked(info_); + + setPressedState(true); + + // Ripple on mouse position by default. + QPoint pos = event->pos(); + qreal radiusEndValue = static_cast(width()) / 2; + + Ripple *ripple = new Ripple(pos); + + ripple->setRadiusEndValue(radiusEndValue); + ripple->setOpacityStartValue(0.35); + ripple->setColor(QColor("#171919")); + ripple->radiusAnimation()->setDuration(300); + ripple->opacityAnimation()->setDuration(500); + + ripple_overlay_->addRipple(ripple); +} + +void RoomInfoListItem::setElidedText(QLabel *label, QString text, int width) +{ + QFontMetrics metrics(label->font()); + QString elidedText = metrics.elidedText(text, Qt::ElideRight, width); + label->setText(elidedText); +} + +RoomInfoListItem::~RoomInfoListItem() +{ +} diff --git a/src/RoomList.cc b/src/RoomList.cc new file mode 100644 index 00000000..1e147a48 --- /dev/null +++ b/src/RoomList.cc @@ -0,0 +1,119 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "ui_RoomList.h" + +#include +#include +#include + +#include "RoomInfoListItem.h" +#include "RoomList.h" +#include "Sync.h" + +RoomList::RoomList(QWidget *parent) + : QWidget(parent) + , ui(new Ui::RoomList) +{ + ui->setupUi(this); +} + +RoomList::~RoomList() +{ + delete ui; +} + +RoomInfo RoomList::extractRoomInfo(const State &room_state) +{ + RoomInfo info; + + auto events = room_state.events(); + + for (int i = 0; i < events.count(); i++) { + if (events[i].type() == "m.room.name") { + info.setName(events[i].content().value("name").toString()); + } else if (events[i].type() == "m.room.topic") { + info.setTopic(events[i].content().value("topic").toString()); + } else if (events[i].type() == "m.room.avatar") { + info.setAvatarUrl(QUrl(events[i].content().value("url").toString())); + } else if (events[i].type() == "m.room.canonical_alias") { + if (info.name().isEmpty()) + info.setName(events[i].content().value("alias").toString()); + } + } + + return info; +} + +void RoomList::setInitialRooms(const Rooms &rooms) +{ + available_rooms_.clear(); + + for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) { + RoomInfo info = RoomList::extractRoomInfo(it.value().state()); + info.setId(it.key()); + + if (info.name().isEmpty()) + continue; + + if (!info.avatarUrl().isEmpty()) + emit fetchRoomAvatar(info.id(), info.avatarUrl()); + + RoomInfoListItem *room_item = new RoomInfoListItem(info, ui->scrollArea); + connect(room_item, + SIGNAL(clicked(const RoomInfo &)), + this, + SLOT(highlightSelectedRoom(const RoomInfo &))); + + available_rooms_.insert(it.key(), room_item); + + ui->scrollVerticalLayout->addWidget(room_item); + } + + // TODO: Move this into its own function. + auto first_room = available_rooms_.first(); + first_room->setPressedState(true); + emit roomChanged(first_room->info()); + + ui->scrollVerticalLayout->addStretch(1); +} + +void RoomList::highlightSelectedRoom(const RoomInfo &info) +{ + emit roomChanged(info); + + for (auto it = available_rooms_.constBegin(); it != available_rooms_.constEnd(); it++) { + if (it.key() != info.id()) + it.value()->setPressedState(false); + } +} + +void RoomList::updateRoomAvatar(const QString &roomid, const QImage &avatar_image) +{ + if (!available_rooms_.contains(roomid)) { + qDebug() << "Avatar update on non existent room" << roomid; + return; + } + + auto list_item = available_rooms_.value(roomid); + list_item->setAvatar(avatar_image); +} + +void RoomList::appendRoom(QString name) +{ + Q_UNUSED(name); +} diff --git a/src/SlidingStackWidget.cc b/src/SlidingStackWidget.cc new file mode 100644 index 00000000..c4d2f7cf --- /dev/null +++ b/src/SlidingStackWidget.cc @@ -0,0 +1,151 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "SlidingStackWidget.h" + +SlidingStackWidget::SlidingStackWidget(QWidget *parent) + : QStackedWidget(parent) +{ + window_ = parent; + + if (parent == Q_NULLPTR) { + qDebug() << "Using nullptr for parent"; + window_ = this; + } + + current_position_ = QPoint(0, 0); + speed_ = 400; + now_ = 0; + next_ = 0; + active_ = false; + animation_type_ = QEasingCurve::InOutCirc; +} + +SlidingStackWidget::~SlidingStackWidget() +{ +} + +void SlidingStackWidget::slideInNext() +{ + int now = currentIndex(); + + if (now < count() - 1) + slideInIndex(now + 1); +} + +void SlidingStackWidget::slideInPrevious() +{ + int now = currentIndex(); + + if (now > 0) + slideInIndex(now - 1); +} + +void SlidingStackWidget::slideInIndex(int index, AnimationDirection direction) +{ + // Take into consideration possible index overflow/undeflow. + if (index > count() - 1) { + direction = AnimationDirection::RIGHT_TO_LEFT; + index = index % count(); + } else if (index < 0) { + direction = AnimationDirection::LEFT_TO_RIGHT; + index = (index + count()) % count(); + } + + slideInWidget(widget(index), direction); +} + +void SlidingStackWidget::slideInWidget(QWidget *next_widget, AnimationDirection direction) +{ + // If an animation is currenlty executing we should wait for it to finish before + // another transition can start. + if (active_) + return; + + active_ = true; + + int now = currentIndex(); + int next = indexOf(next_widget); + + if (now == next) { + active_ = false; + return; + } + + int offset_x = frameRect().width(); + + next_widget->setGeometry(0, 0, offset_x, 0); + + if (direction == AnimationDirection::LEFT_TO_RIGHT) { + offset_x = -offset_x; + } + + QPoint pnext = next_widget->pos(); + QPoint pnow = widget(now)->pos(); + current_position_ = pnow; + + // Reposition the next widget outside of the display area. + next_widget->move(pnext.x() - offset_x, pnext.y()); + + // Make the widget visible. + next_widget->show(); + next_widget->raise(); + + // Animate both the next and now widget. + QPropertyAnimation *animation_now = new QPropertyAnimation(widget(now), "pos"); + + animation_now->setDuration(speed_); + animation_now->setEasingCurve(animation_type_); + animation_now->setStartValue(QPoint(pnow.x(), pnow.y())); + animation_now->setEndValue(QPoint(pnow.x() + offset_x, pnow.y())); + + QPropertyAnimation *animation_next = new QPropertyAnimation(next_widget, "pos"); + + animation_next->setDuration(speed_); + animation_next->setEasingCurve(animation_type_); + animation_next->setStartValue(QPoint(pnext.x() - offset_x, pnext.y())); + animation_next->setEndValue(QPoint(pnext.x(), pnext.y())); + + QParallelAnimationGroup *animation_group = new QParallelAnimationGroup; + + animation_group->addAnimation(animation_now); + animation_group->addAnimation(animation_next); + + connect(animation_group, SIGNAL(finished()), this, SLOT(onAnimationFinished())); + + next_ = next; + now_ = now; + animation_group->start(); +} + +void SlidingStackWidget::onAnimationFinished() +{ + setCurrentIndex(next_); + + // The old widget is no longer necessary so we can hide it and + // move it back to its original position. + widget(now_)->hide(); + widget(now_)->move(current_position_); + + active_ = false; + emit animationFinished(); +} + +int SlidingStackWidget::getWidgetIndex(QWidget *widget) +{ + return indexOf(widget); +} diff --git a/src/Sync.cc b/src/Sync.cc new file mode 100644 index 00000000..3ba6d220 --- /dev/null +++ b/src/Sync.cc @@ -0,0 +1,290 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "Deserializable.h" +#include "Sync.h" + +QString SyncResponse::nextBatch() const +{ + return next_batch_; +} + +void SyncResponse::deserialize(QJsonDocument data) throw(DeserializationException) +{ + if (!data.isObject()) + throw DeserializationException("Sync response is not a JSON object"); + + QJsonObject object = data.object(); + + if (object.value("next_batch") == QJsonValue::Undefined) + throw DeserializationException("Sync: missing next_batch parameter"); + + if (object.value("rooms") == QJsonValue::Undefined) + throw DeserializationException("Sync: missing rooms parameter"); + + rooms_.deserialize(object.value("rooms")); + next_batch_ = object.value("next_batch").toString(); +} + +Rooms SyncResponse::rooms() const +{ + return rooms_; +} + +QMap Rooms::join() const +{ + return join_; +} + +void Rooms::deserialize(QJsonValue data) throw(DeserializationException) +{ + if (!data.isObject()) + throw DeserializationException("Rooms value is not a JSON object"); + + QJsonObject object = data.toObject(); + + if (!object.contains("join")) + throw DeserializationException("rooms/join is missing"); + + if (!object.contains("invite")) + throw DeserializationException("rooms/invite is missing"); + + if (!object.contains("leave")) + throw DeserializationException("rooms/leave is missing"); + + if (!object.value("join").isObject()) + throw DeserializationException("rooms/join must be a JSON object"); + + if (!object.value("invite").isObject()) + throw DeserializationException("rooms/invite must be a JSON object"); + + if (!object.value("leave").isObject()) + throw DeserializationException("rooms/leave must be a JSON object"); + + auto join = object.value("join").toObject(); + + for (auto it = join.constBegin(); it != join.constEnd(); it++) { + JoinedRoom tmp_room; + + try { + tmp_room.deserialize(it.value()); + join_.insert(it.key(), tmp_room); + } catch (DeserializationException &e) { + qWarning() << e.what(); + qWarning() << "Skipping malformed object for room" << it.key(); + } + } +} + +State JoinedRoom::state() const +{ + return state_; +} + +Timeline JoinedRoom::timeline() const +{ + return timeline_; +} + +void JoinedRoom::deserialize(QJsonValue data) throw(DeserializationException) +{ + if (!data.isObject()) + throw DeserializationException("JoinedRoom is not a JSON object"); + + QJsonObject object = data.toObject(); + + if (!object.contains("state")) + throw DeserializationException("join/state is missing"); + + if (!object.contains("timeline")) + throw DeserializationException("join/timeline is missing"); + + if (!object.contains("account_data")) + throw DeserializationException("join/account_data is missing"); + + if (!object.contains("unread_notifications")) + throw DeserializationException("join/unread_notifications is missing"); + + if (!object.value("state").isObject()) + throw DeserializationException("join/state should be an object"); + + QJsonObject state = object.value("state").toObject(); + + if (!state.contains("events")) + throw DeserializationException("join/state/events is missing"); + + state_.deserialize(state.value("events")); + timeline_.deserialize(object.value("timeline")); +} + +QJsonObject Event::content() const +{ + return content_; +} + +QJsonObject Event::unsigned_content() const +{ + return unsigned_; +} + +QString Event::sender() const +{ + return sender_; +} + +QString Event::state_key() const +{ + return state_key_; +} + +QString Event::type() const +{ + return type_; +} + +QString Event::eventId() const +{ + return event_id_; +} + +uint64_t Event::timestamp() const +{ + return origin_server_ts_; +} + +void Event::deserialize(QJsonValue data) throw(DeserializationException) +{ + if (!data.isObject()) + throw DeserializationException("Event is not a JSON object"); + + QJsonObject object = data.toObject(); + + if (!object.contains("content")) + throw DeserializationException("event/content is missing"); + + if (!object.contains("unsigned")) + throw DeserializationException("event/content is missing"); + + if (!object.contains("sender")) + throw DeserializationException("event/sender is missing"); + + if (!object.contains("event_id")) + throw DeserializationException("event/event_id is missing"); + + // TODO: Make this optional + /* if (!object.contains("state_key")) */ + /* throw DeserializationException("event/state_key is missing"); */ + + if (!object.contains("type")) + throw DeserializationException("event/type is missing"); + + if (!object.contains("origin_server_ts")) + throw DeserializationException("event/origin_server_ts is missing"); + + content_ = object.value("content").toObject(); + unsigned_ = object.value("unsigned").toObject(); + + sender_ = object.value("sender").toString(); + state_key_ = object.value("state_key").toString(); + type_ = object.value("type").toString(); + event_id_ = object.value("event_id").toString(); + + origin_server_ts_ = object.value("origin_server_ts").toDouble(); +} + +QList State::events() const +{ + return events_; +} + +void State::deserialize(QJsonValue data) throw(DeserializationException) +{ + if (!data.isArray()) + throw DeserializationException("State is not a JSON array"); + + QJsonArray event_array = data.toArray(); + + for (int i = 0; i < event_array.count(); i++) { + Event event; + + try { + event.deserialize(event_array.at(i)); + events_.push_back(event); + } catch (DeserializationException &e) { + qWarning() << e.what(); + qWarning() << "Skipping malformed state event"; + } + } +} + +QList Timeline::events() const +{ + return events_; +} + +QString Timeline::previousBatch() const +{ + return prev_batch_; +} + +bool Timeline::limited() const +{ + return limited_; +} + +void Timeline::deserialize(QJsonValue data) throw(DeserializationException) +{ + if (!data.isObject()) + throw DeserializationException("Timeline is not a JSON object"); + + auto object = data.toObject(); + + if (!object.contains("events")) + throw DeserializationException("timeline/events is missing"); + + if (!object.contains("prev_batch")) + throw DeserializationException("timeline/prev_batch is missing"); + + if (!object.contains("limited")) + throw DeserializationException("timeline/limited is missing"); + + prev_batch_ = object.value("prev_batch").toString(); + limited_ = object.value("limited").toBool(); + + if (!object.value("events").isArray()) + throw DeserializationException("timeline/events is not a JSON array"); + + auto timeline_events = object.value("events").toArray(); + + for (int i = 0; i < timeline_events.count(); i++) { + Event event; + + try { + event.deserialize(timeline_events.at(i)); + events_.push_back(event); + } catch (DeserializationException &e) { + qWarning() << e.what(); + qWarning() << "Skipping malformed timeline event"; + } + } +} diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc new file mode 100644 index 00000000..ec92e77d --- /dev/null +++ b/src/TextInputWidget.cc @@ -0,0 +1,91 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "TextInputWidget.h" + +TextInputWidget::TextInputWidget(QWidget *parent) + : QWidget(parent) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setCursor(Qt::ArrowCursor); + setStyleSheet("background-color: #171919; height: 45px;"); + + top_layout_ = new QHBoxLayout(); + top_layout_->setSpacing(6); + top_layout_->setContentsMargins(6, 0, 0, 0); + + send_file_button_ = new FlatButton(this); + send_file_button_->setCursor(Qt::PointingHandCursor); + + QIcon send_file_icon; + send_file_icon.addFile(":/icons/icons/clip-dark.png", QSize(), QIcon::Normal, QIcon::Off); + send_file_button_->setForegroundColor(QColor("#577275")); + send_file_button_->setIcon(send_file_icon); + send_file_button_->setIconSize(QSize(24, 24)); + + input_ = new QLineEdit(this); + input_->setPlaceholderText("Write a message..."); + input_->setStyleSheet("color: #ebebeb; font-size: 10pt; border-radius: 0; padding: 2px; margin-bottom: 4px;"); + + send_message_button_ = new FlatButton(this); + send_message_button_->setCursor(Qt::PointingHandCursor); + send_message_button_->setForegroundColor(QColor("#577275")); + + QIcon send_message_icon; + send_message_icon.addFile(":/icons/icons/share-dark.png", QSize(), QIcon::Normal, QIcon::Off); + send_message_button_->setIcon(send_message_icon); + send_message_button_->setIconSize(QSize(24, 24)); + + top_layout_->addWidget(send_file_button_); + top_layout_->addWidget(input_); + top_layout_->addWidget(send_message_button_); + + setLayout(top_layout_); + + connect(send_message_button_, SIGNAL(clicked()), this, SLOT(onSendButtonClicked())); + connect(input_, SIGNAL(returnPressed()), send_message_button_, SIGNAL(clicked())); +} + +void TextInputWidget::onSendButtonClicked() +{ + auto msg_text = input_->text(); + + if (msg_text.isEmpty()) + return; + + emit sendTextMessage(msg_text); + input_->clear(); +} + +void TextInputWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QStyleOption option; + option.initFrom(this); + + QPainter painter(this); + style()->drawPrimitive(QStyle::PE_Widget, &option, &painter, this); +} + +TextInputWidget::~TextInputWidget() +{ +} diff --git a/src/TopRoomBar.cc b/src/TopRoomBar.cc new file mode 100644 index 00000000..7e390bdf --- /dev/null +++ b/src/TopRoomBar.cc @@ -0,0 +1,93 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "TopRoomBar.h" + +TopRoomBar::TopRoomBar(QWidget *parent) + : QWidget(parent) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setMinimumSize(QSize(0, 70)); + setStyleSheet("background-color: #171919; color: #ebebeb;"); + + top_layout_ = new QHBoxLayout(); + top_layout_->setSpacing(10); + top_layout_->setContentsMargins(10, 10, 0, 10); + + avatar_ = new Avatar(this); + avatar_->setLetter(QChar('?')); + avatar_->setBackgroundColor(QColor("#ebebeb")); + avatar_->setSize(45); + + text_layout_ = new QVBoxLayout(); + text_layout_->setSpacing(0); + text_layout_->setContentsMargins(0, 0, 0, 0); + + name_label_ = new QLabel(this); + name_label_->setStyleSheet("font-size: 11pt;"); + + topic_label_ = new QLabel(this); + topic_label_->setStyleSheet("font-size: 10pt; color: #6c7278;"); + + text_layout_->addWidget(name_label_); + text_layout_->addWidget(topic_label_); + + settings_button_ = new FlatButton(this); + settings_button_->setForegroundColor(QColor("#ebebeb")); + settings_button_->setCursor(QCursor(Qt::PointingHandCursor)); + settings_button_->setStyleSheet("width: 30px; height: 30px;"); + + QIcon settings_icon; + settings_icon.addFile(":/icons/icons/cog.png", QSize(), QIcon::Normal, QIcon::Off); + settings_button_->setIcon(settings_icon); + settings_button_->setIconSize(QSize(16, 16)); + + search_button_ = new FlatButton(this); + search_button_->setForegroundColor(QColor("#ebebeb")); + search_button_->setCursor(QCursor(Qt::PointingHandCursor)); + search_button_->setStyleSheet("width: 30px; height: 30px;"); + + QIcon search_icon; + search_icon.addFile(":/icons/icons/search.png", QSize(), QIcon::Normal, QIcon::Off); + search_button_->setIcon(search_icon); + search_button_->setIconSize(QSize(16, 16)); + + top_layout_->addWidget(avatar_); + top_layout_->addLayout(text_layout_); + top_layout_->addStretch(1); + top_layout_->addWidget(search_button_); + top_layout_->addWidget(settings_button_); + + setLayout(top_layout_); +} + +void TopRoomBar::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QStyleOption option; + option.initFrom(this); + + QPainter painter(this); + style()->drawPrimitive(QStyle::PE_Widget, &option, &painter, this); +} + +TopRoomBar::~TopRoomBar() +{ +} diff --git a/src/UserInfoWidget.cc b/src/UserInfoWidget.cc new file mode 100644 index 00000000..a617d212 --- /dev/null +++ b/src/UserInfoWidget.cc @@ -0,0 +1,104 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "UserInfoWidget.h" +#include "FlatButton.h" + +UserInfoWidget::UserInfoWidget(QWidget *parent) + : QWidget(parent) + , display_name_("User") + , userid_("@user:homeserver.org") +{ + QSizePolicy sizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + + setSizePolicy(sizePolicy); + setMinimumSize(QSize(0, 65)); + + topLayout_ = new QHBoxLayout(this); + topLayout_->setSpacing(0); + topLayout_->setContentsMargins(5, 5, 5, 5); + + avatarLayout_ = new QHBoxLayout(); + textLayout_ = new QVBoxLayout(); + + userAvatar_ = new Avatar(this); + userAvatar_->setLetter(QChar('?')); + userAvatar_->setSize(50); + userAvatar_->setMaximumSize(QSize(55, 55)); + + displayNameLabel_ = new QLabel(this); + displayNameLabel_->setStyleSheet( + "padding: 0 9px;" + "color: #ebebeb;" + "font-size: 11pt;" + "margin-bottom: -10px;"); + displayNameLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignTop); + + userIdLabel_ = new QLabel(this); + userIdLabel_->setStyleSheet( + "padding: 0 8px 8px 8px;" + "color: #5D6565;" + "font-size: 10pt;"); + userIdLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter); + + avatarLayout_->addWidget(userAvatar_); + textLayout_->addWidget(displayNameLabel_); + textLayout_->addWidget(userIdLabel_); + + topLayout_->addLayout(avatarLayout_); + topLayout_->addLayout(textLayout_); + topLayout_->addStretch(1); + + buttonLayout_ = new QHBoxLayout(); + + settingsButton_ = new FlatButton(this); + settingsButton_->setForegroundColor(QColor("#ebebeb")); + settingsButton_->setCursor(QCursor(Qt::PointingHandCursor)); + settingsButton_->setStyleSheet("width: 30px; height: 30px;"); + + QIcon icon; + icon.addFile(":/icons/icons/user-shape.png", QSize(), QIcon::Normal, QIcon::Off); + + settingsButton_->setIcon(icon); + settingsButton_->setIconSize(QSize(16, 16)); + + buttonLayout_->addWidget(settingsButton_); + + topLayout_->addLayout(buttonLayout_); +} + +UserInfoWidget::~UserInfoWidget() +{ +} + +void UserInfoWidget::setAvatar(const QImage &img) +{ + avatar_image_ = img; + userAvatar_->setImage(img); +} + +void UserInfoWidget::setDisplayName(const QString &name) +{ + display_name_ = name; + displayNameLabel_->setText(name); +} + +void UserInfoWidget::setUserId(const QString &userid) +{ + userid_ = userid; + userIdLabel_->setText(userid); +} diff --git a/src/WelcomePage.cc b/src/WelcomePage.cc new file mode 100644 index 00000000..2220fad7 --- /dev/null +++ b/src/WelcomePage.cc @@ -0,0 +1,103 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "WelcomePage.h" + +WelcomePage::WelcomePage(QWidget *parent) + : QWidget(parent) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + top_layout_ = new QVBoxLayout(this); + top_layout_->setSpacing(0); + top_layout_->setMargin(0); + + intro_banner_ = new QLabel(this); + intro_banner_->setStyleSheet("background-color: #1c3133;"); + intro_banner_->setAlignment(Qt::AlignCenter); + + intro_text_ = new QLabel(this); + intro_text_->setText(QApplication::translate("WelcomePage", + "" + "" + "" + "

nheko

" + "

" + " " + " A desktop client for Matrix, the open protocol for decentralized communication." + " " + "

\n" + "

" + " Enjoy your stay!" + "

" + "" + "", + Q_NULLPTR)); + + top_layout_->addWidget(intro_banner_); + top_layout_->addWidget(intro_text_, 0, Qt::AlignCenter); + + button_layout_ = new QHBoxLayout(); + button_layout_->setSpacing(0); + button_layout_->setContentsMargins(0, 20, 0, 80); + + register_button_ = new RaisedButton("REGISTER", this); + register_button_->setBackgroundColor(QColor("#171919")); + register_button_->setForegroundColor(QColor("#ebebeb")); + register_button_->setMinimumSize(240, 60); + register_button_->setCursor(QCursor(Qt::PointingHandCursor)); + register_button_->setFontSize(14); + register_button_->setCornerRadius(3); + + login_button_ = new RaisedButton("LOGIN", this); + login_button_->setBackgroundColor(QColor("#171919")); + login_button_->setForegroundColor(QColor("#ebebeb")); + login_button_->setMinimumSize(240, 60); + login_button_->setCursor(QCursor(Qt::PointingHandCursor)); + login_button_->setFontSize(14); + login_button_->setCornerRadius(3); + + button_spacer_ = new QSpacerItem(20, 20, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); + + button_layout_->addStretch(1); + button_layout_->addWidget(register_button_); + button_layout_->addItem(button_spacer_); + button_layout_->addWidget(login_button_); + button_layout_->addStretch(1); + + top_layout_->addLayout(button_layout_); + + connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked())); + connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked())); +} + +void WelcomePage::onLoginButtonClicked() +{ + emit userLogin(); +} + +void WelcomePage::onRegisterButtonClicked() +{ + emit userRegister(); +} + +WelcomePage::~WelcomePage() +{ +} diff --git a/src/main.cc b/src/main.cc new file mode 100644 index 00000000..9ef7ffd7 --- /dev/null +++ b/src/main.cc @@ -0,0 +1,48 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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 "MainWindow.h" + +int main(int argc, char *argv[]) +{ + QCoreApplication::setApplicationName("nheko"); + QCoreApplication::setApplicationVersion("Ωμέγa"); + QCoreApplication::setOrganizationName("Nheko"); + + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Light.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Regular.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Italic.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Bold.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-BoldItalic.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Semibold.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-SemiboldItalic.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-ExtraBold.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-ExtraBoldItalic.ttf"); + + QApplication app(argc, argv); + + QFont font("Open Sans"); + app.setFont(font); + + MainWindow w; + w.show(); + + return app.exec(); +} diff --git a/src/ui/Avatar.cc b/src/ui/Avatar.cc new file mode 100644 index 00000000..4245c168 --- /dev/null +++ b/src/ui/Avatar.cc @@ -0,0 +1,143 @@ +#include +#include +#include + +#include "Avatar.h" + +Avatar::Avatar(QWidget *parent) + : QWidget(parent) +{ + size_ = ui::AvatarSize; + type_ = ui::AvatarType::Letter; + letter_ = QChar('A'); + + QFont _font(font()); + _font.setPointSizeF(ui::FontSize); + setFont(_font); + + QSizePolicy policy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + setSizePolicy(policy); +} + +Avatar::~Avatar() +{ +} + +QColor Avatar::textColor() const +{ + if (!text_color_.isValid()) + return QColor("black"); + + return text_color_; +} + +QColor Avatar::backgroundColor() const +{ + if (!text_color_.isValid()) + return QColor("white"); + + return background_color_; +} + +int Avatar::size() const +{ + return size_; +} + +QSize Avatar::sizeHint() const +{ + return QSize(size_ + 2, size_ + 2); +} + +void Avatar::setTextColor(const QColor &color) +{ + text_color_ = color; +} + +void Avatar::setBackgroundColor(const QColor &color) +{ + background_color_ = color; +} + +void Avatar::setSize(int size) +{ + size_ = size; + + if (!image_.isNull()) { + pixmap_ = QPixmap::fromImage( + image_.scaled(size_, size_, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } + + QFont _font(font()); + _font.setPointSizeF(size_ * (ui::FontSize) / 40); + + setFont(_font); + update(); +} + +void Avatar::setLetter(const QChar &letter) +{ + letter_ = letter; + type_ = ui::AvatarType::Letter; + update(); +} + +void Avatar::setImage(const QImage &image) +{ + image_ = image; + type_ = ui::AvatarType::Image; + pixmap_ = QPixmap::fromImage( + image_.scaled(size_, size_, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + update(); +} + +void Avatar::setIcon(const QIcon &icon) +{ + icon_ = icon; + type_ = ui::AvatarType::Icon; + update(); +} + +void Avatar::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QRect r = rect(); + const int hs = size_ / 2; + + if (type_ != ui::AvatarType::Image) { + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(backgroundColor()); + + painter.setPen(Qt::NoPen); + painter.setBrush(brush); + painter.drawEllipse(r.center(), hs, hs); + } + + switch (type_) { + case ui::AvatarType::Icon: { + icon_.paint(&painter, + QRect((width() - hs) / 2, (height() - hs) / 2, hs, hs), + Qt::AlignCenter, + QIcon::Normal); + break; + } + case ui::AvatarType::Image: { + QPainterPath ppath; + ppath.addEllipse(width() / 2 - hs, height() / 2 - hs, size_, size_); + painter.setClipPath(ppath); + painter.drawPixmap(QRect(width() / 2 - hs, height() / 2 - hs, size_, size_), pixmap_); + break; + } + case ui::AvatarType::Letter: { + painter.setPen(textColor()); + painter.setBrush(Qt::NoBrush); + painter.drawText(r.translated(0, -1), Qt::AlignCenter, letter_); + break; + } + default: + break; + } +} diff --git a/src/ui/Badge.cc b/src/ui/Badge.cc new file mode 100644 index 00000000..05531f6c --- /dev/null +++ b/src/ui/Badge.cc @@ -0,0 +1,186 @@ +#include + +#include "Badge.h" + +Badge::Badge(QWidget *parent) + : OverlayWidget(parent) +{ + init(); +} + +Badge::Badge(const QIcon &icon, QWidget *parent) + : OverlayWidget(parent) +{ + init(); + setIcon(icon); +} + +Badge::Badge(const QString &text, QWidget *parent) + : OverlayWidget(parent) +{ + init(); + setText(text); +} + +Badge::~Badge() +{ +} + +void Badge::init() +{ + x_ = 0; + y_ = 0; + padding_ = 10; + + setAttribute(Qt::WA_TransparentForMouseEvents); + + QFont _font(font()); + _font.setPointSizeF(10); + _font.setStyleName("Bold"); + + setFont(_font); + setText(""); +} + +QString Badge::text() const +{ + return text_; +} + +QIcon Badge::icon() const +{ + return icon_; +} + +QSize Badge::sizeHint() const +{ + const int d = getDiameter(); + return QSize(d + 4, d + 4); +} + +qreal Badge::relativeYPosition() const +{ + return y_; +} + +qreal Badge::relativeXPosition() const +{ + return x_; +} + +QPointF Badge::relativePosition() const +{ + return QPointF(x_, y_); +} + +QColor Badge::backgroundColor() const +{ + if (!background_color_.isValid()) + return QColor("black"); + + return background_color_; +} + +QColor Badge::textColor() const +{ + if (!text_color_.isValid()) + return QColor("white"); + + return text_color_; +} + +void Badge::setTextColor(const QColor &color) +{ + text_color_ = color; +} + +void Badge::setBackgroundColor(const QColor &color) +{ + background_color_ = color; +} + +void Badge::setRelativePosition(const QPointF &pos) +{ + setRelativePosition(pos.x(), pos.y()); +} + +void Badge::setRelativePosition(qreal x, qreal y) +{ + x_ = x; + y_ = y; + update(); +} + +void Badge::setRelativeXPosition(qreal x) +{ + x_ = x; + update(); +} + +void Badge::setRelativeYPosition(qreal y) +{ + y_ = y; + update(); +} + +void Badge::setIcon(const QIcon &icon) +{ + icon_ = icon; + update(); +} + +void Badge::setText(const QString &text) +{ + text_ = text; + + if (!icon_.isNull()) + icon_ = QIcon(); + + size_ = fontMetrics().size(Qt::TextShowMnemonic, text); + + update(); +} + +void Badge::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.translate(x_, y_); + + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(isEnabled() ? backgroundColor() : QColor("#cccccc")); + + painter.setBrush(brush); + painter.setPen(Qt::NoPen); + + const int d = getDiameter(); + + QRectF r(0, 0, d, d); + r.translate(QPointF((width() - d), (height() - d)) / 2); + + if (icon_.isNull()) { + painter.drawEllipse(r); + painter.setPen(textColor()); + painter.setBrush(Qt::NoBrush); + painter.drawText(r.translated(0, -0.5), Qt::AlignCenter, text_); + } else { + painter.drawEllipse(r); + QRectF q(0, 0, 16, 16); + q.moveCenter(r.center()); + QPixmap pixmap = icon().pixmap(16, 16); + QPainter icon(&pixmap); + icon.setCompositionMode(QPainter::CompositionMode_SourceIn); + icon.fillRect(pixmap.rect(), textColor()); + painter.drawPixmap(q.toRect(), pixmap); + } +} + +int Badge::getDiameter() const +{ + if (icon_.isNull()) { + return qMax(size_.width(), size_.height()) + padding_; + } + // FIXME: Move this to Theme.h as the default + return 24; +} diff --git a/src/ui/FlatButton.cc b/src/ui/FlatButton.cc new file mode 100644 index 00000000..97711de5 --- /dev/null +++ b/src/ui/FlatButton.cc @@ -0,0 +1,761 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "FlatButton.h" +#include "Ripple.h" +#include "RippleOverlay.h" +#include "ThemeManager.h" + +void FlatButton::init() +{ + ripple_overlay_ = new RippleOverlay(this); + state_machine_ = new FlatButtonStateMachine(this); + role_ = ui::Default; + ripple_style_ = ui::PositionedRipple; + icon_placement_ = ui::LeftIcon; + overlay_style_ = ui::GrayOverlay; + bg_mode_ = Qt::TransparentMode; + fixed_ripple_radius_ = 64; + corner_radius_ = 3; + base_opacity_ = 0.13; + font_size_ = 10; // 10.5; + use_fixed_ripple_radius_ = false; + halo_visible_ = false; + + setStyle(&ThemeManager::instance()); + setAttribute(Qt::WA_Hover); + setMouseTracking(true); + + QPainterPath path; + path.addRoundedRect(rect(), corner_radius_, corner_radius_); + + ripple_overlay_->setClipPath(path); + ripple_overlay_->setClipping(true); + + state_machine_->setupProperties(); + state_machine_->startAnimations(); +} + +FlatButton::FlatButton(QWidget *parent, ui::ButtonPreset preset) + : QPushButton(parent) +{ + init(); + applyPreset(preset); +} + +FlatButton::FlatButton(const QString &text, QWidget *parent, ui::ButtonPreset preset) + : QPushButton(text, parent) +{ + init(); + applyPreset(preset); +} + +FlatButton::FlatButton(const QString &text, ui::Role role, QWidget *parent, ui::ButtonPreset preset) + : QPushButton(text, parent) +{ + init(); + applyPreset(preset); + setRole(role); +} + +FlatButton::~FlatButton() +{ +} + +void FlatButton::applyPreset(ui::ButtonPreset preset) +{ + switch (preset) { + case ui::FlatPreset: + setOverlayStyle(ui::NoOverlay); + break; + case ui::CheckablePreset: + setOverlayStyle(ui::NoOverlay); + setCheckable(true); + setHaloVisible(false); + break; + default: + break; + } +} + +void FlatButton::setRole(ui::Role role) +{ + role_ = role; + state_machine_->setupProperties(); +} + +ui::Role FlatButton::role() const +{ + return role_; +} + +void FlatButton::setForegroundColor(const QColor &color) +{ + foreground_color_ = color; +} + +QColor FlatButton::foregroundColor() const +{ + if (!foreground_color_.isValid()) { + if (bg_mode_ == Qt::OpaqueMode) { + return ThemeManager::instance().themeColor("BrightWhite"); + } + + switch (role_) { + case ui::Primary: + return ThemeManager::instance().themeColor("Blue"); + case ui::Secondary: + return ThemeManager::instance().themeColor("Gray"); + case ui::Default: + default: + return ThemeManager::instance().themeColor("Black"); + } + } + + return foreground_color_; +} + +void FlatButton::setBackgroundColor(const QColor &color) +{ + background_color_ = color; +} + +QColor FlatButton::backgroundColor() const +{ + if (!background_color_.isValid()) { + switch (role_) { + case ui::Primary: + return ThemeManager::instance().themeColor("Blue"); + case ui::Secondary: + return ThemeManager::instance().themeColor("Gray"); + case ui::Default: + default: + return ThemeManager::instance().themeColor("Black"); + } + } + + return background_color_; +} + +void FlatButton::setOverlayColor(const QColor &color) +{ + overlay_color_ = color; + setOverlayStyle(ui::TintedOverlay); +} + +QColor FlatButton::overlayColor() const +{ + if (!overlay_color_.isValid()) { + return foregroundColor(); + } + + return overlay_color_; +} + +void FlatButton::setDisabledForegroundColor(const QColor &color) +{ + disabled_color_ = color; +} + +QColor FlatButton::disabledForegroundColor() const +{ + if (!disabled_color_.isValid()) { + return ThemeManager::instance().themeColor("FadedWhite"); + } + + return disabled_color_; +} + +void FlatButton::setDisabledBackgroundColor(const QColor &color) +{ + disabled_background_color_ = color; +} + +QColor FlatButton::disabledBackgroundColor() const +{ + if (!disabled_background_color_.isValid()) { + return ThemeManager::instance().themeColor("FadedWhite"); + } + + return disabled_background_color_; +} + +void FlatButton::setFontSize(qreal size) +{ + font_size_ = size; + + QFont f(font()); + f.setPointSizeF(size); + setFont(f); + + update(); +} + +qreal FlatButton::fontSize() const +{ + return font_size_; +} + +void FlatButton::setHaloVisible(bool visible) +{ + halo_visible_ = visible; + update(); +} + +bool FlatButton::isHaloVisible() const +{ + return halo_visible_; +} + +void FlatButton::setOverlayStyle(ui::OverlayStyle style) +{ + overlay_style_ = style; + update(); +} + +ui::OverlayStyle FlatButton::overlayStyle() const +{ + return overlay_style_; +} + +void FlatButton::setRippleStyle(ui::RippleStyle style) +{ + ripple_style_ = style; +} + +ui::RippleStyle FlatButton::rippleStyle() const +{ + return ripple_style_; +} + +void FlatButton::setIconPlacement(ui::ButtonIconPlacement placement) +{ + icon_placement_ = placement; + update(); +} + +ui::ButtonIconPlacement FlatButton::iconPlacement() const +{ + return icon_placement_; +} + +void FlatButton::setCornerRadius(qreal radius) +{ + corner_radius_ = radius; + updateClipPath(); + update(); +} + +qreal FlatButton::cornerRadius() const +{ + return corner_radius_; +} + +void FlatButton::setBackgroundMode(Qt::BGMode mode) +{ + bg_mode_ = mode; + state_machine_->setupProperties(); +} + +Qt::BGMode FlatButton::backgroundMode() const +{ + return bg_mode_; +} + +void FlatButton::setBaseOpacity(qreal opacity) +{ + base_opacity_ = opacity; + state_machine_->setupProperties(); +} + +qreal FlatButton::baseOpacity() const +{ + return base_opacity_; +} + +void FlatButton::setCheckable(bool value) +{ + state_machine_->updateCheckedStatus(); + state_machine_->setCheckedOverlayProgress(0); + + QPushButton::setCheckable(value); +} + +void FlatButton::setHasFixedRippleRadius(bool value) +{ + use_fixed_ripple_radius_ = value; +} + +bool FlatButton::hasFixedRippleRadius() const +{ + return use_fixed_ripple_radius_; +} + +void FlatButton::setFixedRippleRadius(qreal radius) +{ + fixed_ripple_radius_ = radius; + setHasFixedRippleRadius(true); +} + +QSize FlatButton::sizeHint() const +{ + ensurePolished(); + + QSize label(fontMetrics().size(Qt::TextSingleLine, text())); + + int w = 20 + label.width(); + int h = label.height(); + + if (!icon().isNull()) { + w += iconSize().width() + FlatButton::IconPadding; + h = qMax(h, iconSize().height()); + } + + return QSize(w, 20 + h); +} + +void FlatButton::checkStateSet() +{ + state_machine_->updateCheckedStatus(); + QPushButton::checkStateSet(); +} + +void FlatButton::mousePressEvent(QMouseEvent *event) +{ + if (ui::NoRipple != ripple_style_) { + QPoint pos; + qreal radiusEndValue; + + if (ui::CenteredRipple == ripple_style_) { + pos = rect().center(); + } else { + pos = event->pos(); + } + + if (use_fixed_ripple_radius_) { + radiusEndValue = fixed_ripple_radius_; + } else { + radiusEndValue = static_cast(width()) / 2; + } + + Ripple *ripple = new Ripple(pos); + + ripple->setRadiusEndValue(radiusEndValue); + ripple->setOpacityStartValue(0.35); + ripple->setColor(foregroundColor()); + ripple->radiusAnimation()->setDuration(600); + ripple->opacityAnimation()->setDuration(1300); + + ripple_overlay_->addRipple(ripple); + } + + QPushButton::mousePressEvent(event); +} + +void FlatButton::mouseReleaseEvent(QMouseEvent *event) +{ + QPushButton::mouseReleaseEvent(event); + state_machine_->updateCheckedStatus(); +} + +void FlatButton::resizeEvent(QResizeEvent *event) +{ + QPushButton::resizeEvent(event); + updateClipPath(); +} + +void FlatButton::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + const qreal cr = corner_radius_; + + if (cr > 0) { + QPainterPath path; + path.addRoundedRect(rect(), cr, cr); + + painter.setClipPath(path); + painter.setClipping(true); + } + + paintBackground(&painter); + paintHalo(&painter); + + painter.setOpacity(1); + painter.setClipping(false); + + paintForeground(&painter); +} + +void FlatButton::paintBackground(QPainter *painter) +{ + const qreal overlayOpacity = state_machine_->overlayOpacity(); + const qreal checkedProgress = state_machine_->checkedOverlayProgress(); + + if (Qt::OpaqueMode == bg_mode_) { + QBrush brush; + brush.setStyle(Qt::SolidPattern); + + if (isEnabled()) { + brush.setColor(backgroundColor()); + } else { + brush.setColor(disabledBackgroundColor()); + } + + painter->setOpacity(1); + painter->setBrush(brush); + painter->setPen(Qt::NoPen); + painter->drawRect(rect()); + } + + QBrush brush; + brush.setStyle(Qt::SolidPattern); + painter->setPen(Qt::NoPen); + + if (!isEnabled()) { + return; + } + + if ((ui::NoOverlay != overlay_style_) && (overlayOpacity > 0)) { + if (ui::TintedOverlay == overlay_style_) { + brush.setColor(overlayColor()); + } else { + brush.setColor(Qt::gray); + } + + painter->setOpacity(overlayOpacity); + painter->setBrush(brush); + painter->drawRect(rect()); + } + + if (isCheckable() && checkedProgress > 0) { + const qreal q = Qt::TransparentMode == bg_mode_ ? 0.45 : 0.7; + brush.setColor(foregroundColor()); + painter->setOpacity(q * checkedProgress); + painter->setBrush(brush); + QRect r(rect()); + r.setHeight(static_cast(r.height()) * checkedProgress); + painter->drawRect(r); + } +} + +void FlatButton::paintHalo(QPainter *painter) +{ + if (!halo_visible_) + return; + + const qreal opacity = state_machine_->haloOpacity(); + const qreal s = state_machine_->haloScaleFactor() * state_machine_->haloSize(); + const qreal radius = static_cast(width()) * s; + + if (isEnabled() && opacity > 0) { + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(foregroundColor()); + painter->setOpacity(opacity); + painter->setBrush(brush); + painter->setPen(Qt::NoPen); + const QPointF center = rect().center(); + painter->drawEllipse(center, radius, radius); + } +} + +#define COLOR_INTERPOLATE(CH) (1 - progress) * source.CH() + progress *dest.CH() + +void FlatButton::paintForeground(QPainter *painter) +{ + if (isEnabled()) { + painter->setPen(foregroundColor()); + const qreal progress = state_machine_->checkedOverlayProgress(); + + if (isCheckable() && progress > 0) { + QColor source = foregroundColor(); + QColor dest = Qt::TransparentMode == bg_mode_ ? Qt::white + : backgroundColor(); + if (qFuzzyCompare(1, progress)) { + painter->setPen(dest); + } else { + painter->setPen(QColor(COLOR_INTERPOLATE(red), + COLOR_INTERPOLATE(green), + COLOR_INTERPOLATE(blue), + COLOR_INTERPOLATE(alpha))); + } + } + } else { + painter->setPen(disabledForegroundColor()); + } + + if (icon().isNull()) { + painter->drawText(rect(), Qt::AlignCenter, text()); + return; + } + + QSize textSize(fontMetrics().size(Qt::TextSingleLine, text())); + QSize base(size() - textSize); + + const int iw = iconSize().width() + IconPadding; + QPoint pos((base.width() - iw) / 2, 0); + + QRect textGeometry(pos + QPoint(0, base.height() / 2), textSize); + QRect iconGeometry(pos + QPoint(0, (height() - iconSize().height()) / 2), iconSize()); + + if (ui::LeftIcon == icon_placement_) { + textGeometry.translate(iw, 0); + } else { + iconGeometry.translate(textSize.width() + IconPadding, 0); + } + + painter->drawText(textGeometry, Qt::AlignCenter, text()); + + QPixmap pixmap = icon().pixmap(iconSize()); + QPainter icon(&pixmap); + icon.setCompositionMode(QPainter::CompositionMode_SourceIn); + icon.fillRect(pixmap.rect(), painter->pen().color()); + painter->drawPixmap(iconGeometry, pixmap); +} + +void FlatButton::updateClipPath() +{ + const qreal radius = corner_radius_; + + QPainterPath path; + path.addRoundedRect(rect(), radius, radius); + ripple_overlay_->setClipPath(path); +} + +FlatButtonStateMachine::FlatButtonStateMachine(FlatButton *parent) + : QStateMachine(parent) + , button_(parent) + , top_level_state_(new QState(QState::ParallelStates)) + , config_state_(new QState(top_level_state_)) + , checkable_state_(new QState(top_level_state_)) + , checked_state_(new QState(checkable_state_)) + , unchecked_state_(new QState(checkable_state_)) + , neutral_state_(new QState(config_state_)) + , neutral_focused_state_(new QState(config_state_)) + , hovered_state_(new QState(config_state_)) + , hovered_focused_state_(new QState(config_state_)) + , pressed_state_(new QState(config_state_)) + , halo_animation_(new QSequentialAnimationGroup(this)) + , overlay_opacity_(0) + , checked_overlay_progress_(parent->isChecked() ? 1 : 0) + , halo_opacity_(0) + , halo_size_(0.8) + , halo_scale_factor_(1) + , was_checked_(false) +{ + Q_ASSERT(parent); + + parent->installEventFilter(this); + + config_state_->setInitialState(neutral_state_); + addState(top_level_state_); + setInitialState(top_level_state_); + + checkable_state_->setInitialState(parent->isChecked() ? checked_state_ + : unchecked_state_); + QSignalTransition *transition; + QPropertyAnimation *animation; + + transition = new QSignalTransition(this, SIGNAL(buttonChecked())); + transition->setTargetState(checked_state_); + unchecked_state_->addTransition(transition); + + animation = new QPropertyAnimation(this, "checkedOverlayProgress", this); + animation->setDuration(200); + transition->addAnimation(animation); + + transition = new QSignalTransition(this, SIGNAL(buttonUnchecked())); + transition->setTargetState(unchecked_state_); + checked_state_->addTransition(transition); + + animation = new QPropertyAnimation(this, "checkedOverlayProgress", this); + animation->setDuration(200); + transition->addAnimation(animation); + + addTransition(button_, QEvent::FocusIn, neutral_state_, neutral_focused_state_); + addTransition(button_, QEvent::FocusOut, neutral_focused_state_, neutral_state_); + addTransition(button_, QEvent::Enter, neutral_state_, hovered_state_); + addTransition(button_, QEvent::Leave, hovered_state_, neutral_state_); + addTransition(button_, QEvent::Enter, neutral_focused_state_, hovered_focused_state_); + addTransition(button_, QEvent::Leave, hovered_focused_state_, neutral_focused_state_); + addTransition(button_, QEvent::FocusIn, hovered_state_, hovered_focused_state_); + addTransition(button_, QEvent::FocusOut, hovered_focused_state_, hovered_state_); + addTransition(this, SIGNAL(buttonPressed()), hovered_state_, pressed_state_); + addTransition(button_, QEvent::Leave, pressed_state_, neutral_focused_state_); + addTransition(button_, QEvent::FocusOut, pressed_state_, hovered_state_); + + neutral_state_->assignProperty(this, "haloSize", 0); + neutral_focused_state_->assignProperty(this, "haloSize", 0.7); + hovered_state_->assignProperty(this, "haloSize", 0); + pressed_state_->assignProperty(this, "haloSize", 4); + hovered_focused_state_->assignProperty(this, "haloSize", 0.7); + + QPropertyAnimation *grow = new QPropertyAnimation(this); + QPropertyAnimation *shrink = new QPropertyAnimation(this); + + grow->setTargetObject(this); + grow->setPropertyName("haloScaleFactor"); + grow->setStartValue(0.56); + grow->setEndValue(0.63); + grow->setEasingCurve(QEasingCurve::InOutSine); + grow->setDuration(840); + + shrink->setTargetObject(this); + shrink->setPropertyName("haloScaleFactor"); + shrink->setStartValue(0.63); + shrink->setEndValue(0.56); + shrink->setEasingCurve(QEasingCurve::InOutSine); + shrink->setDuration(840); + + halo_animation_->addAnimation(grow); + halo_animation_->addAnimation(shrink); + halo_animation_->setLoopCount(-1); +} + +FlatButtonStateMachine::~FlatButtonStateMachine() +{ +} + +void FlatButtonStateMachine::setOverlayOpacity(qreal opacity) +{ + overlay_opacity_ = opacity; + button_->update(); +} + +void FlatButtonStateMachine::setCheckedOverlayProgress(qreal opacity) +{ + checked_overlay_progress_ = opacity; + button_->update(); +} + +void FlatButtonStateMachine::setHaloOpacity(qreal opacity) +{ + halo_opacity_ = opacity; + button_->update(); +} + +void FlatButtonStateMachine::setHaloSize(qreal size) +{ + halo_size_ = size; + button_->update(); +} + +void FlatButtonStateMachine::setHaloScaleFactor(qreal factor) +{ + halo_scale_factor_ = factor; + button_->update(); +} + +void FlatButtonStateMachine::startAnimations() +{ + halo_animation_->start(); + start(); +} + +void FlatButtonStateMachine::setupProperties() +{ + QColor overlayColor; + + if (Qt::TransparentMode == button_->backgroundMode()) { + overlayColor = button_->backgroundColor(); + } else { + overlayColor = button_->foregroundColor(); + } + + const qreal baseOpacity = button_->baseOpacity(); + + neutral_state_->assignProperty(this, "overlayOpacity", 0); + neutral_state_->assignProperty(this, "haloOpacity", 0); + neutral_focused_state_->assignProperty(this, "overlayOpacity", 0); + neutral_focused_state_->assignProperty(this, "haloOpacity", baseOpacity); + hovered_state_->assignProperty(this, "overlayOpacity", baseOpacity); + hovered_state_->assignProperty(this, "haloOpacity", 0); + hovered_focused_state_->assignProperty(this, "overlayOpacity", baseOpacity); + hovered_focused_state_->assignProperty(this, "haloOpacity", baseOpacity); + pressed_state_->assignProperty(this, "overlayOpacity", baseOpacity); + pressed_state_->assignProperty(this, "haloOpacity", 0); + checked_state_->assignProperty(this, "checkedOverlayProgress", 1); + unchecked_state_->assignProperty(this, "checkedOverlayProgress", 0); + + button_->update(); +} + +void FlatButtonStateMachine::updateCheckedStatus() +{ + const bool checked = button_->isChecked(); + if (was_checked_ != checked) { + was_checked_ = checked; + if (checked) { + emit buttonChecked(); + } else { + emit buttonUnchecked(); + } + } +} + +bool FlatButtonStateMachine::eventFilter(QObject *watched, + QEvent *event) +{ + if (QEvent::FocusIn == event->type()) { + QFocusEvent *focusEvent = static_cast(event); + if (focusEvent && Qt::MouseFocusReason == focusEvent->reason()) { + emit buttonPressed(); + return true; + } + } + + return QStateMachine::eventFilter(watched, event); +} + +void FlatButtonStateMachine::addTransition(QObject *object, + const char *signal, + QState *fromState, + QState *toState) +{ + addTransition(new QSignalTransition(object, signal), fromState, toState); +} + +void FlatButtonStateMachine::addTransition(QObject *object, + QEvent::Type eventType, + QState *fromState, + QState *toState) +{ + addTransition(new QEventTransition(object, eventType), fromState, toState); +} + +void FlatButtonStateMachine::addTransition(QAbstractTransition *transition, + QState *fromState, + QState *toState) +{ + transition->setTargetState(toState); + + QPropertyAnimation *animation; + + animation = new QPropertyAnimation(this, "overlayOpacity", this); + animation->setDuration(150); + transition->addAnimation(animation); + + animation = new QPropertyAnimation(this, "haloOpacity", this); + animation->setDuration(170); + transition->addAnimation(animation); + + animation = new QPropertyAnimation(this, "haloSize", this); + animation->setDuration(350); + animation->setEasingCurve(QEasingCurve::OutCubic); + transition->addAnimation(animation); + + fromState->addTransition(transition); +} diff --git a/src/ui/OverlayWidget.cc b/src/ui/OverlayWidget.cc new file mode 100644 index 00000000..b4dfb918 --- /dev/null +++ b/src/ui/OverlayWidget.cc @@ -0,0 +1,59 @@ +#include "OverlayWidget.h" +#include + +OverlayWidget::OverlayWidget(QWidget *parent) + : QWidget(parent) +{ + if (parent) + parent->installEventFilter(this); +} + +OverlayWidget::~OverlayWidget() +{ +} + +bool OverlayWidget::event(QEvent *event) +{ + if (!parent()) + return QWidget::event(event); + + switch (event->type()) { + case QEvent::ParentChange: { + parent()->installEventFilter(this); + setGeometry(overlayGeometry()); + break; + } + case QEvent::ParentAboutToChange: { + parent()->removeEventFilter(this); + break; + } + default: + break; + } + + return QWidget::event(event); +} + +bool OverlayWidget::eventFilter(QObject *obj, QEvent *event) +{ + switch (event->type()) { + case QEvent::Move: + case QEvent::Resize: + setGeometry(overlayGeometry()); + break; + default: + break; + } + + return QWidget::eventFilter(obj, event); +} + +QRect OverlayWidget::overlayGeometry() const +{ + QWidget *widget = parentWidget(); + + if (!widget) + return QRect(); + + return widget->rect(); +} diff --git a/src/ui/RaisedButton.cc b/src/ui/RaisedButton.cc new file mode 100644 index 00000000..74f549c4 --- /dev/null +++ b/src/ui/RaisedButton.cc @@ -0,0 +1,92 @@ +#include +#include +#include +#include +#include + +#include "RaisedButton.h" + +void RaisedButton::init() +{ + shadow_state_machine_ = new QStateMachine(this); + normal_state_ = new QState; + pressed_state_ = new QState; + effect_ = new QGraphicsDropShadowEffect; + + effect_->setBlurRadius(7); + effect_->setOffset(QPointF(0, 2)); + effect_->setColor(QColor(0, 0, 0, 75)); + + setBackgroundMode(Qt::OpaqueMode); + setMinimumHeight(42); + setGraphicsEffect(effect_); + setBaseOpacity(0.3); + + shadow_state_machine_->addState(normal_state_); + shadow_state_machine_->addState(pressed_state_); + + normal_state_->assignProperty(effect_, "offset", QPointF(0, 2)); + normal_state_->assignProperty(effect_, "blurRadius", 7); + + pressed_state_->assignProperty(effect_, "offset", QPointF(0, 5)); + pressed_state_->assignProperty(effect_, "blurRadius", 29); + + QAbstractTransition *transition; + + transition = new QEventTransition(this, QEvent::MouseButtonPress); + transition->setTargetState(pressed_state_); + normal_state_->addTransition(transition); + + transition = new QEventTransition(this, QEvent::MouseButtonDblClick); + transition->setTargetState(pressed_state_); + normal_state_->addTransition(transition); + + transition = new QEventTransition(this, QEvent::MouseButtonRelease); + transition->setTargetState(normal_state_); + pressed_state_->addTransition(transition); + + QPropertyAnimation *animation; + + animation = new QPropertyAnimation(effect_, "offset", this); + animation->setDuration(100); + shadow_state_machine_->addDefaultAnimation(animation); + + animation = new QPropertyAnimation(effect_, "blurRadius", this); + animation->setDuration(100); + shadow_state_machine_->addDefaultAnimation(animation); + + shadow_state_machine_->setInitialState(normal_state_); + shadow_state_machine_->start(); +} + +RaisedButton::RaisedButton(QWidget *parent) + : FlatButton(parent) +{ + init(); +} + +RaisedButton::RaisedButton(const QString &text, QWidget *parent) + : FlatButton(parent) +{ + init(); + setText(text); +} + +RaisedButton::~RaisedButton() +{ +} + +bool RaisedButton::event(QEvent *event) +{ + if (QEvent::EnabledChange == event->type()) { + if (isEnabled()) { + shadow_state_machine_->start(); + effect_->setEnabled(true); + } else { + shadow_state_machine_->stop(); + effect_->setEnabled(false); + } + } + + return FlatButton::event(event); +} diff --git a/src/ui/Ripple.cc b/src/ui/Ripple.cc new file mode 100644 index 00000000..107bfd7f --- /dev/null +++ b/src/ui/Ripple.cc @@ -0,0 +1,106 @@ +#include "Ripple.h" +#include "RippleOverlay.h" + +Ripple::Ripple(const QPoint ¢er, QObject *parent) + : QParallelAnimationGroup(parent) + , overlay_(0) + , radius_anim_(animate("radius")) + , opacity_anim_(animate("opacity")) + , radius_(0) + , opacity_(0) + , center_(center) +{ + init(); +} + +Ripple::Ripple(const QPoint ¢er, RippleOverlay *overlay, QObject *parent) + : QParallelAnimationGroup(parent) + , overlay_(overlay) + , radius_anim_(animate("radius")) + , opacity_anim_(animate("opacity")) + , radius_(0) + , opacity_(0) + , center_(center) +{ + init(); +} + +Ripple::~Ripple() +{ +} + +void Ripple::setRadius(qreal radius) +{ + Q_ASSERT(overlay_); + + if (radius_ == radius) + return; + + radius_ = radius; + overlay_->update(); +} + +void Ripple::setOpacity(qreal opacity) +{ + Q_ASSERT(overlay_); + + if (opacity_ == opacity) + return; + + opacity_ = opacity; + overlay_->update(); +} + +void Ripple::setColor(const QColor &color) +{ + if (brush_.color() == color) + return; + + brush_.setColor(color); + + if (overlay_) + overlay_->update(); +} + +void Ripple::setBrush(const QBrush &brush) +{ + brush_ = brush; + + if (overlay_) + overlay_->update(); +} + +void Ripple::destroy() +{ + Q_ASSERT(overlay_); + + overlay_->removeRipple(this); +} + +QPropertyAnimation *Ripple::animate(const QByteArray &property, + const QEasingCurve &easing, + int duration) +{ + QPropertyAnimation *animation = new QPropertyAnimation; + animation->setTargetObject(this); + animation->setPropertyName(property); + animation->setEasingCurve(easing); + animation->setDuration(duration); + + addAnimation(animation); + + return animation; +} + +void Ripple::init() +{ + setOpacityStartValue(0.5); + setOpacityEndValue(0); + setRadiusStartValue(0); + setRadiusEndValue(300); + + brush_.setColor(Qt::black); + brush_.setStyle(Qt::SolidPattern); + + connect(this, SIGNAL(finished()), this, SLOT(destroy())); +} diff --git a/src/ui/RippleOverlay.cc b/src/ui/RippleOverlay.cc new file mode 100644 index 00000000..add030d9 --- /dev/null +++ b/src/ui/RippleOverlay.cc @@ -0,0 +1,61 @@ +#include + +#include "Ripple.h" +#include "RippleOverlay.h" + +RippleOverlay::RippleOverlay(QWidget *parent) + : OverlayWidget(parent) + , use_clip_(false) +{ + setAttribute(Qt::WA_TransparentForMouseEvents); + setAttribute(Qt::WA_NoSystemBackground); +} + +RippleOverlay::~RippleOverlay() +{ +} + +void RippleOverlay::addRipple(Ripple *ripple) +{ + ripple->setOverlay(this); + ripples_.push_back(ripple); + ripple->start(); +} + +void RippleOverlay::addRipple(const QPoint &position, qreal radius) +{ + Ripple *ripple = new Ripple(position); + ripple->setRadiusEndValue(radius); + addRipple(ripple); +} + +void RippleOverlay::removeRipple(Ripple *ripple) +{ + if (ripples_.removeOne(ripple)) + delete ripple; +} + +void RippleOverlay::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.setPen(Qt::NoPen); + + if (use_clip_) + painter.setClipPath(clip_path_); + + for (auto it = ripples_.constBegin(); it != ripples_.constEnd(); it++) + paintRipple(&painter, *it); +} + +void RippleOverlay::paintRipple(QPainter *painter, Ripple *ripple) +{ + const qreal radius = ripple->radius(); + const QPointF center = ripple->center(); + + painter->setOpacity(ripple->opacity()); + painter->setBrush(ripple->brush()); + painter->drawEllipse(center, radius, radius); +} diff --git a/src/ui/TextField.cc b/src/ui/TextField.cc new file mode 100644 index 00000000..3b701549 --- /dev/null +++ b/src/ui/TextField.cc @@ -0,0 +1,346 @@ +#include "TextField.h" + +#include +#include +#include +#include +#include +#include + +TextField::TextField(QWidget *parent) + : QLineEdit(parent) +{ + state_machine_ = new TextFieldStateMachine(this); + label_ = 0; + label_font_size_ = 9.5; + show_label_ = false; + background_color_ = QColor("white"); + + setFrame(false); + setAttribute(Qt::WA_Hover); + setMouseTracking(true); + setTextMargins(0, 2, 0, 4); + + QFontDatabase db; + QFont font(db.font("Open Sans", "Regular", 11)); + setFont(font); + + state_machine_->start(); + QCoreApplication::processEvents(); +} + +TextField::~TextField() +{ +} + +void TextField::setBackgroundColor(const QColor &color) +{ + background_color_ = color; +} + +QColor TextField::backgroundColor() const +{ + return background_color_; +} + +void TextField::setShowLabel(bool value) +{ + if (show_label_ == value) { + return; + } + + show_label_ = value; + + if (!label_ && value) { + label_ = new TextFieldLabel(this); + state_machine_->setLabel(label_); + } + + if (value) { + setContentsMargins(0, 23, 0, 0); + } else { + setContentsMargins(0, 0, 0, 0); + } +} + +bool TextField::hasLabel() const +{ + return show_label_; +} + +void TextField::setLabelFontSize(qreal size) +{ + label_font_size_ = size; + + if (label_) { + QFont font(label_->font()); + font.setPointSizeF(size); + label_->setFont(font); + label_->update(); + } +} + +qreal TextField::labelFontSize() const +{ + return label_font_size_; +} + +void TextField::setLabel(const QString &label) +{ + label_text_ = label; + setShowLabel(true); + label_->update(); +} + +QString TextField::label() const +{ + return label_text_; +} + +void TextField::setTextColor(const QColor &color) +{ + text_color_ = color; + setStyleSheet(QString("QLineEdit { color: %1; }").arg(color.name())); +} + +QColor TextField::textColor() const +{ + if (!text_color_.isValid()) { + return QColor("black"); + } + + return text_color_; +} + +void TextField::setLabelColor(const QColor &color) +{ + label_color_ = color; +} + +QColor TextField::labelColor() const +{ + if (!label_color_.isValid()) { + return QColor("#abb"); // TODO: Move this into Theme.h + } + + return label_color_; +} + +void TextField::setInkColor(const QColor &color) +{ + ink_color_ = color; +} + +QColor TextField::inkColor() const +{ + if (!ink_color_.isValid()) { + return QColor("black"); + } + + return ink_color_; +} + +void TextField::setUnderlineColor(const QColor &color) +{ + underline_color_ = color; +} + +QColor TextField::underlineColor() const +{ + if (!underline_color_.isValid()) { + return QColor("black"); + } + + return underline_color_; +} + +bool TextField::event(QEvent *event) +{ + switch (event->type()) { + case QEvent::Resize: + case QEvent::Move: { + if (label_) + label_->setGeometry(rect()); + break; + } + default: + break; + } + + return QLineEdit::event(event); +} + +void TextField::paintEvent(QPaintEvent *event) +{ + QLineEdit::paintEvent(event); + + QPainter painter(this); + + if (text().isEmpty()) { + painter.setOpacity(1 - state_machine_->progress()); + //painter.fillRect(rect(), parentWidget()->palette().color(backgroundRole())); + painter.fillRect(rect(), backgroundColor()); + } + + const int y = height() - 1; + const int wd = width() - 5; + + QPen pen; + pen.setWidth(1); + pen.setColor(underlineColor()); + painter.setPen(pen); + painter.setOpacity(1); + painter.drawLine(2.5, y, wd, y); + + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(inkColor()); + + const qreal progress = state_machine_->progress(); + + if (progress > 0) { + painter.setPen(Qt::NoPen); + painter.setBrush(brush); + const int w = (1 - progress) * static_cast(wd / 2); + painter.drawRect(w + 2.5, height() - 2, wd - 2 * w, 2); + } +} + +TextFieldStateMachine::TextFieldStateMachine(TextField *parent) + : QStateMachine(parent), text_field_(parent) +{ + normal_state_ = new QState; + focused_state_ = new QState; + + label_ = 0; + offset_anim_ = 0; + color_anim_ = 0; + progress_ = 0.0; + + addState(normal_state_); + addState(focused_state_); + + setInitialState(normal_state_); + + QEventTransition *transition; + QPropertyAnimation *animation; + + transition = new QEventTransition(parent, QEvent::FocusIn); + transition->setTargetState(focused_state_); + normal_state_->addTransition(transition); + + animation = new QPropertyAnimation(this, "progress", this); + animation->setEasingCurve(QEasingCurve::InCubic); + animation->setDuration(310); + transition->addAnimation(animation); + + transition = new QEventTransition(parent, QEvent::FocusOut); + transition->setTargetState(normal_state_); + focused_state_->addTransition(transition); + + animation = new QPropertyAnimation(this, "progress", this); + animation->setEasingCurve(QEasingCurve::OutCubic); + animation->setDuration(310); + transition->addAnimation(animation); + + normal_state_->assignProperty(this, "progress", 0); + focused_state_->assignProperty(this, "progress", 1); + + setupProperties(); + + connect(text_field_, SIGNAL(textChanged(QString)), this, SLOT(setupProperties())); +} + +TextFieldStateMachine::~TextFieldStateMachine() +{ +} + +void TextFieldStateMachine::setLabel(TextFieldLabel *label) +{ + if (label_) { + delete label_; + } + + if (offset_anim_) { + removeDefaultAnimation(offset_anim_); + delete offset_anim_; + } + + if (color_anim_) { + removeDefaultAnimation(color_anim_); + delete color_anim_; + } + + label_ = label; + + if (label_) { + offset_anim_ = new QPropertyAnimation(label_, "offset", this); + offset_anim_->setDuration(210); + offset_anim_->setEasingCurve(QEasingCurve::OutCubic); + addDefaultAnimation(offset_anim_); + + color_anim_ = new QPropertyAnimation(label_, "color", this); + color_anim_->setDuration(210); + addDefaultAnimation(color_anim_); + } + + setupProperties(); +} + +void TextFieldStateMachine::setupProperties() +{ + if (label_) { + const int m = text_field_->textMargins().top(); + + if (text_field_->text().isEmpty()) { + normal_state_->assignProperty(label_, "offset", QPointF(0, 26)); + } else { + normal_state_->assignProperty(label_, "offset", QPointF(0, 0 - m)); + } + + focused_state_->assignProperty(label_, "offset", QPointF(0, 0 - m)); + focused_state_->assignProperty(label_, "color", text_field_->inkColor()); + normal_state_->assignProperty(label_, "color", text_field_->labelColor()); + + if (0 != label_->offset().y() && !text_field_->text().isEmpty()) { + label_->setOffset(QPointF(0, 0 - m)); + } else if (!text_field_->hasFocus() && label_->offset().y() <= 0 && text_field_->text().isEmpty()) { + label_->setOffset(QPointF(0, 26)); + } + } + + text_field_->update(); +} + +TextFieldLabel::TextFieldLabel(TextField *parent) + : QWidget(parent), text_field_(parent) +{ + x_ = 0; + y_ = 26; + scale_ = 1; + color_ = parent->labelColor(); + + QFontDatabase db; + QFont font(db.font("Open Sans", "Medium", parent->labelFontSize())); + font.setLetterSpacing(QFont::PercentageSpacing, 102); + setFont(font); +} + +TextFieldLabel::~TextFieldLabel() +{ +} + +void TextFieldLabel::paintEvent(QPaintEvent *) +{ + if (!text_field_->hasLabel()) + return; + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.scale(scale_, scale_); + painter.setPen(color_); + painter.setOpacity(1); + + QPointF pos(2 + x_, height() - 36 + y_); + painter.drawText(pos.x(), pos.y(), text_field_->label()); +} diff --git a/src/ui/Theme.cc b/src/ui/Theme.cc new file mode 100644 index 00000000..4c5c19de --- /dev/null +++ b/src/ui/Theme.cc @@ -0,0 +1,73 @@ +#include + +#include "Theme.h" + +Theme::Theme(QObject *parent) + : QObject(parent) +{ + setColor("Black", ui::Color::Black); + + setColor("BrightWhite", ui::Color::BrightWhite); + setColor("FadedWhite", ui::Color::FadedWhite); + setColor("MediumWhite", ui::Color::MediumWhite); + + setColor("BrightGreen", ui::Color::BrightGreen); + setColor("DarkGreen", ui::Color::DarkGreen); + setColor("LightGreen", ui::Color::LightGreen); + + setColor("Gray", ui::Color::Gray); + setColor("Red", ui::Color::Red); + setColor("Blue", ui::Color::Blue); + + setColor("Transparent", ui::Color::Transparent); +} + +Theme::~Theme() +{ +} + +QColor Theme::rgba(int r, int g, int b, qreal a) const +{ + QColor color(r, g, b); + color.setAlphaF(a); + + return color; +} + +QColor Theme::getColor(const QString &key) const +{ + if (!colors_.contains(key)) { + qWarning() << "Color with key" << key << "could not be found"; + return QColor(); + } + + return colors_.value(key); +} + +void Theme::setColor(const QString &key, const QColor &color) +{ + colors_.insert(key, color); +} + +void Theme::setColor(const QString &key, ui::Color &color) +{ + static const QColor palette[] = { + QColor("#171919"), + + QColor("#EBEBEB"), + QColor("#C9C9C9"), + QColor("#929292"), + + QColor("#1C3133"), + QColor("#577275"), + QColor("#46A451"), + + QColor("#5D6565"), + QColor("#E22826"), + QColor("#81B3A9"), + + rgba(0, 0, 0, 0), + }; + + colors_.insert(key, palette[color]); +} diff --git a/src/ui/ThemeManager.cc b/src/ui/ThemeManager.cc new file mode 100644 index 00000000..3c8a16ab --- /dev/null +++ b/src/ui/ThemeManager.cc @@ -0,0 +1,20 @@ +#include + +#include "ThemeManager.h" + +ThemeManager::ThemeManager() +{ + setTheme(new Theme); +} + +void ThemeManager::setTheme(Theme *theme) +{ + theme_ = theme; + theme_->setParent(this); +} + +QColor ThemeManager::themeColor(const QString &key) const +{ + Q_ASSERT(theme_); + return theme_->getColor(key); +}